diff options
Diffstat (limited to 'lib/impure')
-rw-r--r-- | lib/impure/db_mysql.nim | 360 | ||||
-rw-r--r-- | lib/impure/db_odbc.nim | 463 | ||||
-rw-r--r-- | lib/impure/db_postgres.nim | 366 | ||||
-rw-r--r-- | lib/impure/db_sqlite.nim | 291 | ||||
-rw-r--r-- | lib/impure/nre.nim | 665 | ||||
-rw-r--r-- | lib/impure/nre/private/util.nim | 26 | ||||
-rw-r--r-- | lib/impure/osinfo_posix.nim | 10 | ||||
-rw-r--r-- | lib/impure/osinfo_win.nim | 10 | ||||
-rw-r--r-- | lib/impure/rdstdin.nim | 146 | ||||
-rw-r--r-- | lib/impure/re.nim | 648 | ||||
-rw-r--r-- | lib/impure/ssl.nim | 97 |
11 files changed, 786 insertions, 2296 deletions
diff --git a/lib/impure/db_mysql.nim b/lib/impure/db_mysql.nim deleted file mode 100644 index 170fee8b8..000000000 --- a/lib/impure/db_mysql.nim +++ /dev/null @@ -1,360 +0,0 @@ -# -# -# Nim's Runtime Library -# (c) Copyright 2015 Andreas Rumpf -# -# See the file "copying.txt", included in this -# distribution, for details about the copyright. -# - -## A higher level `mySQL`:idx: database wrapper. The same interface is -## implemented for other databases too. -## -## Example: -## -## .. code-block:: Nim -## -## import db_mysql, math -## -## let theDb = open("localhost", "nim", "nim", "test") -## -## theDb.exec(sql"Drop table if exists myTestTbl") -## theDb.exec(sql("create table myTestTbl (" & -## " Id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, " & -## " Name VARCHAR(50) NOT NULL, " & -## " i INT(11), " & -## " f DECIMAL(18,10))")) -## -## theDb.exec(sql"START TRANSACTION") -## for i in 1..1000: -## theDb.exec(sql"INSERT INTO myTestTbl (name,i,f) VALUES (?,?,?)", -## "Item#" & $i, i, sqrt(i.float)) -## theDb.exec(sql"COMMIT") -## -## for x in theDb.fastRows(sql"select * from myTestTbl"): -## echo x -## -## let id = theDb.tryInsertId(sql"INSERT INTO myTestTbl (name,i,f) VALUES (?,?,?)", -## "Item#1001", 1001, sqrt(1001.0)) -## echo "Inserted item: ", theDb.getValue(sql"SELECT name FROM myTestTbl WHERE id=?", id) -## -## theDb.close() - - -import strutils, mysql - -import db_common -export db_common - -type - DbConn* = PMySQL ## encapsulates a database connection - Row* = seq[string] ## a row of a dataset. NULL database values will be - ## converted to nil. - InstantRow* = object ## a handle that can be used to get a row's - ## column text on demand - row: cstringArray - len: int -{.deprecated: [TRow: Row, TDbConn: DbConn].} - -proc dbError*(db: DbConn) {.noreturn.} = - ## raises a DbError exception. - var e: ref DbError - new(e) - e.msg = $mysql.error(db) - raise e - -when false: - proc dbQueryOpt*(db: DbConn, query: string, args: varargs[string, `$`]) = - var stmt = mysql_stmt_init(db) - if stmt == nil: dbError(db) - if mysql_stmt_prepare(stmt, query, len(query)) != 0: - dbError(db) - var - binding: seq[MYSQL_BIND] - discard mysql_stmt_close(stmt) - -proc dbQuote*(s: string): string = - ## DB quotes the string. - result = "'" - for c in items(s): - if c == '\'': add(result, "''") - else: add(result, c) - add(result, '\'') - -proc dbFormat(formatstr: SqlQuery, args: varargs[string]): string = - result = "" - var a = 0 - for c in items(string(formatstr)): - if c == '?': - if args[a] == nil: - add(result, "NULL") - else: - add(result, dbQuote(args[a])) - inc(a) - else: - add(result, c) - -proc tryExec*(db: DbConn, query: SqlQuery, args: varargs[string, `$`]): bool {. - tags: [ReadDbEffect, WriteDbEffect].} = - ## tries to execute the query and returns true if successful, false otherwise. - var q = dbFormat(query, args) - return mysql.realQuery(db, q, q.len) == 0'i32 - -proc rawExec(db: DbConn, query: SqlQuery, args: varargs[string, `$`]) = - var q = dbFormat(query, args) - if mysql.realQuery(db, q, q.len) != 0'i32: dbError(db) - -proc exec*(db: DbConn, query: SqlQuery, args: varargs[string, `$`]) {. - tags: [ReadDbEffect, WriteDbEffect].} = - ## executes the query and raises EDB if not successful. - var q = dbFormat(query, args) - if mysql.realQuery(db, q, q.len) != 0'i32: dbError(db) - -proc newRow(L: int): Row = - newSeq(result, L) - for i in 0..L-1: result[i] = "" - -proc properFreeResult(sqlres: mysql.PRES, row: cstringArray) = - if row != nil: - while mysql.fetchRow(sqlres) != nil: discard - mysql.freeResult(sqlres) - -iterator fastRows*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): Row {.tags: [ReadDbEffect].} = - ## executes the query and iterates over the result dataset. - ## - ## This is very fast, but potentially dangerous. Use this iterator only - ## if you require **ALL** the rows. - ## - ## Breaking the fastRows() iterator during a loop will cause the next - ## database query to raise an [EDb] exception ``Commands out of sync``. - rawExec(db, query, args) - var sqlres = mysql.useResult(db) - if sqlres != nil: - var L = int(mysql.numFields(sqlres)) - var result = newRow(L) - var row: cstringArray - while true: - row = mysql.fetchRow(sqlres) - if row == nil: break - for i in 0..L-1: - setLen(result[i], 0) - if row[i] == nil: - result[i] = nil - else: - add(result[i], row[i]) - yield result - properFreeResult(sqlres, row) - -iterator instantRows*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): InstantRow - {.tags: [ReadDbEffect].} = - ## Same as fastRows but returns a handle that can be used to get column text - ## on demand using []. Returned handle is valid only within the iterator body. - rawExec(db, query, args) - var sqlres = mysql.useResult(db) - if sqlres != nil: - let L = int(mysql.numFields(sqlres)) - var row: cstringArray - while true: - row = mysql.fetchRow(sqlres) - if row == nil: break - yield InstantRow(row: row, len: L) - properFreeResult(sqlres, row) - -proc setTypeName(t: var DbType; f: PFIELD) = - shallowCopy(t.name, $f.name) - t.maxReprLen = Natural(f.max_length) - if (NOT_NULL_FLAG and f.flags) != 0: t.notNull = true - case f.ftype - of TYPE_DECIMAL: - t.kind = dbDecimal - of TYPE_TINY: - t.kind = dbInt - t.size = 1 - of TYPE_SHORT: - t.kind = dbInt - t.size = 2 - of TYPE_LONG: - t.kind = dbInt - t.size = 4 - of TYPE_FLOAT: - t.kind = dbFloat - t.size = 4 - of TYPE_DOUBLE: - t.kind = dbFloat - t.size = 8 - of TYPE_NULL: - t.kind = dbNull - of TYPE_TIMESTAMP: - t.kind = dbTimestamp - of TYPE_LONGLONG: - t.kind = dbInt - t.size = 8 - of TYPE_INT24: - t.kind = dbInt - t.size = 3 - of TYPE_DATE: - t.kind = dbDate - of TYPE_TIME: - t.kind = dbTime - of TYPE_DATETIME: - t.kind = dbDatetime - of TYPE_YEAR: - t.kind = dbDate - of TYPE_NEWDATE: - t.kind = dbDate - of TYPE_VARCHAR, TYPE_VAR_STRING, TYPE_STRING: - t.kind = dbVarchar - of TYPE_BIT: - t.kind = dbBit - of TYPE_NEWDECIMAL: - t.kind = dbDecimal - of TYPE_ENUM: t.kind = dbEnum - of TYPE_SET: t.kind = dbSet - of TYPE_TINY_BLOB, TYPE_MEDIUM_BLOB, TYPE_LONG_BLOB, - TYPE_BLOB: t.kind = dbBlob - of TYPE_GEOMETRY: - t.kind = dbGeometry - -proc setColumnInfo(columns: var DbColumns; res: PRES; L: int) = - setLen(columns, L) - for i in 0..<L: - let fp = mysql.fetch_field_direct(res, cint(i)) - setTypeName(columns[i].typ, fp) - columns[i].name = $fp.name - columns[i].tableName = $fp.table - columns[i].primaryKey = (fp.flags and PRI_KEY_FLAG) != 0 - #columns[i].foreignKey = there is no such thing in mysql - -iterator instantRows*(db: DbConn; columns: var DbColumns; query: SqlQuery; - args: varargs[string, `$`]): InstantRow = - ## Same as fastRows but returns a handle that can be used to get column text - ## on demand using []. Returned handle is valid only within the iterator body. - rawExec(db, query, args) - var sqlres = mysql.useResult(db) - if sqlres != nil: - let L = int(mysql.numFields(sqlres)) - setColumnInfo(columns, sqlres, L) - var row: cstringArray - while true: - row = mysql.fetchRow(sqlres) - if row == nil: break - yield InstantRow(row: row, len: L) - properFreeResult(sqlres, row) - - -proc `[]`*(row: InstantRow, col: int): string {.inline.} = - ## Returns text for given column of the row. - $row.row[col] - -proc len*(row: InstantRow): int {.inline.} = - ## Returns number of columns in the row. - row.len - -proc getRow*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): Row {.tags: [ReadDbEffect].} = - ## Retrieves a single row. If the query doesn't return any rows, this proc - ## will return a Row with empty strings for each column. - rawExec(db, query, args) - var sqlres = mysql.useResult(db) - if sqlres != nil: - var L = int(mysql.numFields(sqlres)) - result = newRow(L) - var row = mysql.fetchRow(sqlres) - if row != nil: - for i in 0..L-1: - setLen(result[i], 0) - if row[i] == nil: - result[i] = nil - else: - add(result[i], row[i]) - properFreeResult(sqlres, row) - -proc getAllRows*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): seq[Row] {.tags: [ReadDbEffect].} = - ## executes the query and returns the whole result dataset. - result = @[] - rawExec(db, query, args) - var sqlres = mysql.useResult(db) - if sqlres != nil: - var L = int(mysql.numFields(sqlres)) - var row: cstringArray - var j = 0 - while true: - row = mysql.fetchRow(sqlres) - if row == nil: break - setLen(result, j+1) - newSeq(result[j], L) - for i in 0..L-1: - if row[i] == nil: - result[j][i] = nil - else: - result[j][i] = $row[i] - inc(j) - mysql.freeResult(sqlres) - -iterator rows*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): Row {.tags: [ReadDbEffect].} = - ## same as `fastRows`, but slower and safe. - for r in items(getAllRows(db, query, args)): yield r - -proc getValue*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): string {.tags: [ReadDbEffect].} = - ## executes the query and returns the first column of the first row of the - ## result dataset. Returns "" if the dataset contains no rows or the database - ## value is NULL. - result = getRow(db, query, args)[0] - -proc tryInsertId*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): int64 {.tags: [WriteDbEffect].} = - ## executes the query (typically "INSERT") and returns the - ## generated ID for the row or -1 in case of an error. - var q = dbFormat(query, args) - if mysql.realQuery(db, q, q.len) != 0'i32: - result = -1'i64 - else: - result = mysql.insertId(db) - -proc insertId*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): int64 {.tags: [WriteDbEffect].} = - ## executes the query (typically "INSERT") and returns the - ## generated ID for the row. - result = tryInsertID(db, query, args) - if result < 0: dbError(db) - -proc execAffectedRows*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): int64 {. - tags: [ReadDbEffect, WriteDbEffect].} = - ## runs the query (typically "UPDATE") and returns the - ## number of affected rows - rawExec(db, query, args) - result = mysql.affectedRows(db) - -proc close*(db: DbConn) {.tags: [DbEffect].} = - ## closes the database connection. - if db != nil: mysql.close(db) - -proc open*(connection, user, password, database: string): DbConn {. - tags: [DbEffect].} = - ## opens a database connection. Raises `EDb` if the connection could not - ## be established. - result = mysql.init(nil) - if result == nil: dbError("could not open database connection") - let - colonPos = connection.find(':') - host = if colonPos < 0: connection - else: substr(connection, 0, colonPos-1) - port: int32 = if colonPos < 0: 0'i32 - else: substr(connection, colonPos+1).parseInt.int32 - if mysql.realConnect(result, host, user, password, database, - port, nil, 0) == nil: - var errmsg = $mysql.error(result) - db_mysql.close(result) - dbError(errmsg) - -proc setEncoding*(connection: DbConn, encoding: string): bool {. - tags: [DbEffect].} = - ## sets the encoding of a database connection, returns true for - ## success, false for failure. - result = mysql.set_character_set(connection, encoding) == 0 diff --git a/lib/impure/db_odbc.nim b/lib/impure/db_odbc.nim deleted file mode 100644 index 6af69d842..000000000 --- a/lib/impure/db_odbc.nim +++ /dev/null @@ -1,463 +0,0 @@ -# -# -# Nim's Runtime Library -# (c) Copyright 2015 Nim Contributors -# -# See the file "copying.txt", included in this -# distribution, for details about the copyright. -# - -## A higher level `ODBC` database wrapper. -## -## This is the same interface that is implemented for other databases. -## -## This has NOT yet been (extensively) tested agains ODBC drivers for -## Teradata, Oracle, Sybase, MSSqlvSvr, et. al. databases -## -## Currently all queries are ANSI calls, not Unicode. -## -## Example: -## -## .. code-block:: Nim -## -## import db_odbc, math -## -## let theDb = open("localhost", "nim", "nim", "test") -## -## theDb.exec(sql"Drop table if exists myTestTbl") -## theDb.exec(sql("create table myTestTbl (" & -## " Id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, " & -## " Name VARCHAR(50) NOT NULL, " & -## " i INT(11), " & -## " f DECIMAL(18,10))")) -## -## theDb.exec(sql"START TRANSACTION") -## for i in 1..1000: -## theDb.exec(sql"INSERT INTO myTestTbl (name,i,f) VALUES (?,?,?)", -## "Item#" & $i, i, sqrt(i.float)) -## theDb.exec(sql"COMMIT") -## -## for x in theDb.fastRows(sql"select * from myTestTbl"): -## echo x -## -## let id = theDb.tryInsertId(sql"INSERT INTO myTestTbl (name,i,f) VALUES (?,?,?)", -## "Item#1001", 1001, sqrt(1001.0)) -## echo "Inserted item: ", theDb.getValue(sql"SELECT name FROM myTestTbl WHERE id=?", id) -## -## theDb.close() - - -import strutils, odbcsql - -import db_common -export db_common - -type - OdbcConnTyp = tuple[hDb: SqlHDBC, env: SqlHEnv, stmt: SqlHStmt] - DbConn* = OdbcConnTyp ## encapsulates a database connection - Row* = seq[string] ## a row of a dataset. NULL database values will be - ## converted to nil. - InstantRow* = tuple[row: seq[string], len: int] ## a handle that can be - ## used to get a row's - ## column text on demand - -{.deprecated: [TRow: Row, TSqlQuery: SqlQuery, TDbConn: DbConn].} - -var - buf: array[0..4096, char] - -proc properFreeResult(hType: int, sqlres: var SqlHandle) {. - tags: [WriteDbEffect], raises: [].} = - try: - discard SQLFreeHandle(hType.TSqlSmallInt, sqlres) - sqlres = nil - except: discard - -proc getErrInfo(db: var DbConn): tuple[res: int, ss, ne, msg: string] {. - tags: [ReadDbEffect], raises: [].} = - ## Returns ODBC error information - var - sqlState: array[0..512, char] - nativeErr: array[0..512, char] - errMsg: array[0..512, char] - retSz: TSqlSmallInt = 0 - res: TSqlSmallInt = 0 - try: - sqlState[0] = '\0' - nativeErr[0] = '\0' - errMsg[0] = '\0' - res = SQLErr(db.env, db.hDb, db.stmt, - cast[PSQLCHAR](sqlState.addr), - cast[PSQLCHAR](nativeErr.addr), - cast[PSQLCHAR](errMsg.addr), - 511.TSqlSmallInt, retSz.addr.PSQLSMALLINT) - except: - discard - return (res.int, $sqlState, $nativeErr, $errMsg) - -proc dbError*(db: var DbConn) {. - tags: [ReadDbEffect, WriteDbEffect], raises: [DbError] .} = - ## Raises an `[DbError]` exception with ODBC error information - var - e: ref DbError - ss, ne, msg: string = "" - isAnError = false - res: int = 0 - prevSs = "" - while true: - prevSs = ss - (res, ss, ne, msg) = db.getErrInfo() - if prevSs == ss: - break - # sqlState of 00000 is not an error - elif ss == "00000": - break - elif ss == "01000": - echo "\nWarning: ", ss, " ", msg - continue - else: - isAnError = true - echo "\nError: ", ss, " ", msg - if isAnError: - new(e) - e.msg = "ODBC Error" - if db.stmt != nil: - properFreeResult(SQL_HANDLE_STMT, db.stmt) - properFreeResult(SQL_HANDLE_DBC, db.hDb) - properFreeResult(SQL_HANDLE_ENV, db.env) - raise e - -proc SqlCheck(db: var DbConn, resVal: TSqlSmallInt) {.raises: [DbError]} = - ## Wrapper that checks if ``resVal`` is not SQL_SUCCESS and if so, raises [EDb] - if resVal != SQL_SUCCESS: dbError(db) - -proc SqlGetDBMS(db: var DbConn): string {. - tags: [ReadDbEffect, WriteDbEffect], raises: [] .} = - ## Returns the ODBC SQL_DBMS_NAME string - const - SQL_DBMS_NAME = 17.SqlUSmallInt - var - sz: TSqlSmallInt = 0 - buf[0] = '\0' - try: - db.SqlCheck(SQLGetInfo(db.hDb, SQL_DBMS_NAME, cast[SqlPointer](buf.addr), - 4095.TSqlSmallInt, sz.addr)) - except: discard - return $buf.cstring - -proc dbQuote*(s: string): string {.noSideEffect.} = - ## DB quotes the string. - result = "'" - for c in items(s): - if c == '\'': add(result, "''") - else: add(result, c) - add(result, '\'') - -proc dbFormat(formatstr: SqlQuery, args: varargs[string]): string {. - noSideEffect.} = - ## Replace any ``?`` placeholders with `args`, - ## and quotes the arguments - result = "" - var a = 0 - for c in items(string(formatstr)): - if c == '?': - if args[a] == nil: - add(result, "NULL") - else: - add(result, dbQuote(args[a])) - inc(a) - else: - add(result, c) - -proc prepareFetch(db: var DbConn, query: SqlQuery, - args: varargs[string, `$`]) {. - tags: [ReadDbEffect, WriteDbEffect], raises: [DbError].} = - # Prepare a statement, execute it and fetch the data to the driver - # ready for retrieval of the data - # Used internally by iterators and retrieval procs - # requires calling - # properFreeResult(SQL_HANDLE_STMT, db.stmt) - # when finished - db.SqlCheck(SQLAllocHandle(SQL_HANDLE_STMT, db.hDb, db.stmt)) - var q = dbFormat(query, args) - db.SqlCheck(SQLPrepare(db.stmt, q.PSQLCHAR, q.len.TSqlSmallInt)) - db.SqlCheck(SQLExecute(db.stmt)) - db.SqlCheck(SQLFetch(db.stmt)) - -proc prepareFetchDirect(db: var DbConn, query: SqlQuery, - args: varargs[string, `$`]) {. - tags: [ReadDbEffect, WriteDbEffect], raises: [DbError].} = - # Prepare a statement, execute it and fetch the data to the driver - # ready for retrieval of the data - # Used internally by iterators and retrieval procs - # requires calling - # properFreeResult(SQL_HANDLE_STMT, db.stmt) - # when finished - db.SqlCheck(SQLAllocHandle(SQL_HANDLE_STMT, db.hDb, db.stmt)) - var q = dbFormat(query, args) - db.SqlCheck(SQLExecDirect(db.stmt, q.PSQLCHAR, q.len.TSqlSmallInt)) - db.SqlCheck(SQLFetch(db.stmt)) - -proc tryExec*(db: var DbConn, query: SqlQuery, args: varargs[string, `$`]): bool {. - tags: [ReadDbEffect, WriteDbEffect], raises: [].} = - ## Tries to execute the query and returns true if successful, false otherwise. - var - res:TSqlSmallInt = -1 - try: - db.prepareFetchDirect(query, args) - var - rCnt = -1 - res = SQLRowCount(db.stmt, rCnt) - if res != SQL_SUCCESS: dbError(db) - properFreeResult(SQL_HANDLE_STMT, db.stmt) - except: discard - return res == SQL_SUCCESS - -proc rawExec(db: var DbConn, query: SqlQuery, args: varargs[string, `$`]) {. - tags: [ReadDbEffect, WriteDbEffect], raises: [DbError].} = - db.prepareFetchDirect(query, args) - -proc exec*(db: var DbConn, query: SqlQuery, args: varargs[string, `$`]) {. - tags: [ReadDbEffect, WriteDbEffect], raises: [DbError].} = - ## Executes the query and raises EDB if not successful. - db.prepareFetchDirect(query, args) - properFreeResult(SQL_HANDLE_STMT, db.stmt) - -proc newRow(L: int): Row {.noSideEFfect.} = - newSeq(result, L) - for i in 0..L-1: result[i] = "" - -iterator fastRows*(db: var DbConn, query: SqlQuery, - args: varargs[string, `$`]): Row {. - tags: [ReadDbEffect, WriteDbEffect], raises: [DbError].} = - ## Executes the query and iterates over the result dataset. - ## - ## This is very fast, but potentially dangerous. Use this iterator only - ## if you require **ALL** the rows. - ## - ## Breaking the fastRows() iterator during a loop may cause a driver error - ## for subsequenct queries - ## - ## Rows are retrieved from the server at each iteration. - var - rowRes: Row - sz: TSqlSmallInt = 0 - cCnt: TSqlSmallInt = 0.TSqlSmallInt - rCnt = -1 - - db.prepareFetch(query, args) - db.SqlCheck(SQLNumResultCols(db.stmt, cCnt)) - db.SqlCheck(SQLRowCount(db.stmt, rCnt)) - rowRes = newRow(cCnt) - for rNr in 1..rCnt: - for colId in 1..cCnt: - buf[0] = '\0' - db.SqlCheck(SQLGetData(db.stmt, colId.SqlUSmallInt, SQL_C_CHAR, - cast[cstring](buf.addr), 4095.TSqlSmallInt, sz.addr)) - rowRes[colId-1] = $buf.cstring - db.SqlCheck(SQLFetchScroll(db.stmt, SQL_FETCH_NEXT, 1)) - yield rowRes - properFreeResult(SQL_HANDLE_STMT, db.stmt) - -iterator instantRows*(db: var DbConn, query: SqlQuery, - args: varargs[string, `$`]): InstantRow - {.tags: [ReadDbEffect, WriteDbEffect].} = - ## Same as fastRows but returns a handle that can be used to get column text - ## on demand using []. Returned handle is valid only within the interator body. - var - rowRes: Row - sz: TSqlSmallInt = 0 - cCnt: TSqlSmallInt = 0.TSqlSmallInt - rCnt = -1 - db.prepareFetch(query, args) - db.SqlCheck(SQLNumResultCols(db.stmt, cCnt)) - db.SqlCheck(SQLRowCount(db.stmt, rCnt)) - rowRes = newRow(cCnt) - for rNr in 1..rCnt: - for colId in 1..cCnt: - buf[0] = '\0' - db.SqlCheck(SQLGetData(db.stmt, colId.SqlUSmallInt, SQL_C_CHAR, - cast[cstring](buf.addr), 4095.TSqlSmallInt, sz.addr)) - rowRes[colId-1] = $buf.cstring - db.SqlCheck(SQLFetchScroll(db.stmt, SQL_FETCH_NEXT, 1)) - yield (row: rowRes, len: cCnt.int) - properFreeResult(SQL_HANDLE_STMT, db.stmt) - -proc `[]`*(row: InstantRow, col: int): string {.inline.} = - ## Returns text for given column of the row - row.row[col] - -proc len*(row: InstantRow): int {.inline.} = - ## Returns number of columns in the row - row.len - -proc getRow*(db: var DbConn, query: SqlQuery, - args: varargs[string, `$`]): Row {. - tags: [ReadDbEffect, WriteDbEffect], raises: [DbError].} = - ## Retrieves a single row. If the query doesn't return any rows, this proc - ## will return a Row with empty strings for each column. - var - sz: TSqlSmallInt = 0.TSqlSmallInt - cCnt: TSqlSmallInt = 0.TSqlSmallInt - rCnt = -1 - result = @[] - db.prepareFetch(query, args) - db.SqlCheck(SQLNumResultCols(db.stmt, cCnt)) - - db.SqlCheck(SQLRowCount(db.stmt, rCnt)) - for colId in 1..cCnt: - db.SqlCheck(SQLGetData(db.stmt, colId.SqlUSmallInt, SQL_C_CHAR, - cast[cstring](buf.addr), 4095.TSqlSmallInt, sz.addr)) - result.add($buf.cstring) - db.SqlCheck(SQLFetchScroll(db.stmt, SQL_FETCH_NEXT, 1)) - properFreeResult(SQL_HANDLE_STMT, db.stmt) - -proc getAllRows*(db: var DbConn, query: SqlQuery, - args: varargs[string, `$`]): seq[Row] {. - tags: [ReadDbEffect, WriteDbEffect], raises: [DbError].} = - ## Executes the query and returns the whole result dataset. - var - rowRes: Row - sz: TSqlSmallInt = 0 - cCnt: TSqlSmallInt = 0.TSqlSmallInt - rCnt = -1 - db.prepareFetch(query, args) - db.SqlCheck(SQLNumResultCols(db.stmt, cCnt)) - db.SqlCheck(SQLRowCount(db.stmt, rCnt)) - result = @[] - for rNr in 1..rCnt: - rowRes = @[] - buf[0] = '\0' - for colId in 1..cCnt: - db.SqlCheck(SQLGetData(db.stmt, colId.SqlUSmallInt, SQL_C_CHAR, - cast[SqlPointer](buf.addr), 4095.TSqlSmallInt, sz.addr)) - rowRes.add($buf.cstring) - db.SqlCheck(SQLFetchScroll(db.stmt, SQL_FETCH_NEXT, 1)) - result.add(rowRes) - properFreeResult(SQL_HANDLE_STMT, db.stmt) - -iterator rows*(db: var DbConn, query: SqlQuery, - args: varargs[string, `$`]): Row {. - tags: [ReadDbEffect, WriteDbEffect], raises: [DbError].} = - ## Same as `fastRows`, but slower and safe. - ## - ## This retrieves ALL rows into memory before - ## iterating through the rows. - ## Large dataset queries will impact on memory usage. - for r in items(getAllRows(db, query, args)): yield r - -proc getValue*(db: var DbConn, query: SqlQuery, - args: varargs[string, `$`]): string {. - tags: [ReadDbEffect, WriteDbEffect], raises: [].} = - ## Executes the query and returns the first column of the first row of the - ## result dataset. Returns "" if the dataset contains no rows or the database - ## value is NULL. - result = "" - try: - result = getRow(db, query, args)[0] - except: discard - -proc tryInsertId*(db: var DbConn, query: SqlQuery, - args: varargs[string, `$`]): int64 {. - tags: [ReadDbEffect, WriteDbEffect], raises: [].} = - ## Executes the query (typically "INSERT") and returns the - ## generated ID for the row or -1 in case of an error. - if not tryExec(db, query, args): - result = -1'i64 - else: - echo "DBMS: ",SqlGetDBMS(db).toLower() - result = -1'i64 - try: - case SqlGetDBMS(db).toLower(): - of "postgresql": - result = getValue(db, sql"SELECT LASTVAL();", []).parseInt - of "mysql": - result = getValue(db, sql"SELECT LAST_INSERT_ID();", []).parseInt - of "sqlite": - result = getValue(db, sql"SELECT LAST_INSERT_ROWID();", []).parseInt - of "microsoft sql server": - result = getValue(db, sql"SELECT SCOPE_IDENTITY();", []).parseInt - of "oracle": - result = getValue(db, sql"SELECT id.currval FROM DUAL;", []).parseInt - else: result = -1'i64 - except: discard - -proc insertId*(db: var DbConn, query: SqlQuery, - args: varargs[string, `$`]): int64 {. - tags: [ReadDbEffect, WriteDbEffect], raises: [DbError].} = - ## Executes the query (typically "INSERT") and returns the - ## generated ID for the row. - result = tryInsertID(db, query, args) - if result < 0: dbError(db) - -proc execAffectedRows*(db: var DbConn, query: SqlQuery, - args: varargs[string, `$`]): int64 {. - tags: [ReadDbEffect, WriteDbEffect], raises: [DbError].} = - ## Runs the query (typically "UPDATE") and returns the - ## number of affected rows - result = -1 - var res = SQLAllocHandle(SQL_HANDLE_STMT, db.hDb, db.stmt.SqlHandle) - if res != SQL_SUCCESS: dbError(db) - var q = dbFormat(query, args) - res = SQLPrepare(db.stmt, q.PSQLCHAR, q.len.TSqlSmallInt) - if res != SQL_SUCCESS: dbError(db) - rawExec(db, query, args) - var rCnt = -1 - result = SQLRowCount(db.hDb, rCnt) - if res != SQL_SUCCESS: dbError(db) - properFreeResult(SQL_HANDLE_STMT, db.stmt) - result = rCnt - -proc close*(db: var DbConn) {. - tags: [WriteDbEffect], raises: [].} = - ## Closes the database connection. - if db.hDb != nil: - try: - var res = SQLDisconnect(db.hDb) - if db.stmt != nil: - res = SQLFreeHandle(SQL_HANDLE_STMT, db.stmt) - res = SQLFreeHandle(SQL_HANDLE_DBC, db.hDb) - res = SQLFreeHandle(SQL_HANDLE_ENV, db.env) - db = (hDb: nil, env: nil, stmt: nil) - except: - discard - -proc open*(connection, user, password, database: string): DbConn {. - tags: [ReadDbEffect, WriteDbEffect], raises: [DbError].} = - ## Opens a database connection. - ## - ## Raises `EDb` if the connection could not be established. - ## - ## Currently the database parameter is ignored, - ## but included to match ``open()`` in the other db_xxxxx library modules. - var - val: TSqlInteger = SQL_OV_ODBC3 - resLen = 0 - result = (hDb: nil, env: nil, stmt: nil) - # allocate environment handle - var res = SQLAllocHandle(SQL_HANDLE_ENV, result.env, result.env) - if res != SQL_SUCCESS: dbError("Error: unable to initialise ODBC environment.") - res = SQLSetEnvAttr(result.env, - SQL_ATTR_ODBC_VERSION.TSqlInteger, - val, resLen.TSqlInteger) - if res != SQL_SUCCESS: dbError("Error: unable to set ODBC driver version.") - # allocate hDb handle - res = SQLAllocHandle(SQL_HANDLE_DBC, result.env, result.hDb) - if res != SQL_SUCCESS: dbError("Error: unable to allocate connection handle.") - - # Connect: connection = dsn str, - res = SQLConnect(result.hDb, - connection.PSQLCHAR , connection.len.TSqlSmallInt, - user.PSQLCHAR, user.len.TSqlSmallInt, - password.PSQLCHAR, password.len.TSqlSmallInt) - if res != SQL_SUCCESS: - result.dbError() - -proc setEncoding*(connection: DbConn, encoding: string): bool {. - tags: [ReadDbEffect, WriteDbEffect], raises: [DbError].} = - ## Currently not implemented for ODBC. - ## - ## Sets the encoding of a database connection, returns true for - ## success, false for failure. - #result = set_character_set(connection, encoding) == 0 - dbError("setEncoding() is currently not implemented by the db_odbc module") diff --git a/lib/impure/db_postgres.nim b/lib/impure/db_postgres.nim deleted file mode 100644 index 9bdbae4c2..000000000 --- a/lib/impure/db_postgres.nim +++ /dev/null @@ -1,366 +0,0 @@ -# -# -# Nim's Runtime Library -# (c) Copyright 2015 Andreas Rumpf -# -# See the file "copying.txt", included in this -# distribution, for details about the copyright. -# - -## A higher level `PostgreSQL`:idx: database wrapper. This interface -## is implemented for other databases also. -## -## Parameter substitution -## ---------------------- -## -## All ``db_*`` modules support the same form of parameter substitution. -## That is, using the ``?`` (question mark) to signify the place where a -## value should be placed. For example: -## -## .. code-block:: Nim -## sql"INSERT INTO myTable (colA, colB, colC) VALUES (?, ?, ?)" -## -## **Note**: There are two approaches to parameter substitution support by -## this module. -## -## 1. ``SqlQuery`` using ``?, ?, ?, ...`` (same as all the ``db_*`` modules) -## -## 2. ``SqlPrepared`` using ``$1, $2, $3, ...`` -## -## .. code-block:: Nim -## prepare(db, "myExampleInsert", -## sql"""INSERT INTO myTable -## (colA, colB, colC) -## VALUES ($1, $2, $3)""", -## 3) -## -## Examples -## -------- -## -## Opening a connection to a database -## ================================== -## -## .. code-block:: Nim -## import db_postgres -## let db = open("localhost", "user", "password", "dbname") -## db.close() -## -## Creating a table -## ================ -## -## .. code-block:: Nim -## db.exec(sql"DROP TABLE IF EXISTS myTable") -## db.exec(sql("""CREATE TABLE myTable ( -## id integer, -## name varchar(50) not null)""")) -## -## Inserting data -## ============== -## -## .. code-block:: Nim -## db.exec(sql"INSERT INTO myTable (id, name) VALUES (0, ?)", -## "Dominik") -import strutils, postgres - -import db_common -export db_common - -type - DbConn* = PPGconn ## encapsulates a database connection - Row* = seq[string] ## a row of a dataset. NULL database values will be - ## converted to nil. - InstantRow* = tuple[res: PPGresult, line: int32] ## a handle that can be - ## used to get a row's - ## column text on demand - SqlPrepared* = distinct string ## a identifier for the prepared queries - -{.deprecated: [TRow: Row, TDbConn: DbConn, - TSqlPrepared: SqlPrepared].} - -proc dbError*(db: DbConn) {.noreturn.} = - ## raises a DbError exception. - var e: ref DbError - new(e) - e.msg = $pqErrorMessage(db) - raise e - -proc dbQuote*(s: string): string = - ## DB quotes the string. - result = "'" - for c in items(s): - if c == '\'': add(result, "''") - else: add(result, c) - add(result, '\'') - -proc dbFormat(formatstr: SqlQuery, args: varargs[string]): string = - result = "" - var a = 0 - if args.len > 0 and not string(formatstr).contains("?"): - dbError("""parameter substitution expects "?" """) - for c in items(string(formatstr)): - if c == '?': - if args[a] == nil: - add(result, "NULL") - else: - add(result, dbQuote(args[a])) - inc(a) - else: - add(result, c) - -proc tryExec*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): bool {.tags: [ReadDbEffect, WriteDbEffect].} = - ## tries to execute the query and returns true if successful, false otherwise. - var res = pqexecParams(db, dbFormat(query, args), 0, nil, nil, - nil, nil, 0) - result = pqresultStatus(res) == PGRES_COMMAND_OK - pqclear(res) - -proc tryExec*(db: DbConn, stmtName: SqlPrepared, - args: varargs[string, `$`]): bool {.tags: [ - ReadDbEffect, WriteDbEffect].} = - ## tries to execute the query and returns true if successful, false otherwise. - var arr = allocCStringArray(args) - var res = pqexecPrepared(db, stmtName.string, int32(args.len), arr, - nil, nil, 0) - deallocCStringArray(arr) - result = pqresultStatus(res) == PGRES_COMMAND_OK - pqclear(res) - -proc exec*(db: DbConn, query: SqlQuery, args: varargs[string, `$`]) {. - tags: [ReadDbEffect, WriteDbEffect].} = - ## executes the query and raises EDB if not successful. - var res = pqexecParams(db, dbFormat(query, args), 0, nil, nil, - nil, nil, 0) - if pqresultStatus(res) != PGRES_COMMAND_OK: dbError(db) - pqclear(res) - -proc exec*(db: DbConn, stmtName: SqlPrepared, - args: varargs[string]) {.tags: [ReadDbEffect, WriteDbEffect].} = - var arr = allocCStringArray(args) - var res = pqexecPrepared(db, stmtName.string, int32(args.len), arr, - nil, nil, 0) - deallocCStringArray(arr) - if pqResultStatus(res) != PGRES_COMMAND_OK: dbError(db) - pqclear(res) - -proc newRow(L: int): Row = - newSeq(result, L) - for i in 0..L-1: result[i] = "" - -proc setupQuery(db: DbConn, query: SqlQuery, - args: varargs[string]): PPGresult = - result = pqexec(db, dbFormat(query, args)) - if pqResultStatus(result) != PGRES_TUPLES_OK: dbError(db) - -proc setupQuery(db: DbConn, stmtName: SqlPrepared, - args: varargs[string]): PPGresult = - var arr = allocCStringArray(args) - result = pqexecPrepared(db, stmtName.string, int32(args.len), arr, - nil, nil, 0) - deallocCStringArray(arr) - if pqResultStatus(result) != PGRES_TUPLES_OK: dbError(db) - -proc prepare*(db: DbConn; stmtName: string, query: SqlQuery; - nParams: int): SqlPrepared = - if nParams > 0 and not string(query).contains("$1"): - dbError("""parameter substitution expects "$1" """) - var res = pqprepare(db, stmtName, query.string, int32(nParams), nil) - if pqResultStatus(res) != PGRES_COMMAND_OK: dbError(db) - return SqlPrepared(stmtName) - -proc setRow(res: PPGresult, r: var Row, line, cols: int32) = - for col in 0..cols-1: - setLen(r[col], 0) - let x = pqgetvalue(res, line, col) - if x.isNil: - r[col] = nil - else: - add(r[col], x) - -iterator fastRows*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): Row {.tags: [ReadDbEffect].} = - ## executes the query and iterates over the result dataset. This is very - ## fast, but potenially dangerous: If the for-loop-body executes another - ## query, the results can be undefined. For Postgres it is safe though. - var res = setupQuery(db, query, args) - var L = pqnfields(res) - var result = newRow(L) - for i in 0..pqntuples(res)-1: - setRow(res, result, i, L) - yield result - pqclear(res) - -iterator fastRows*(db: DbConn, stmtName: SqlPrepared, - args: varargs[string, `$`]): Row {.tags: [ReadDbEffect].} = - ## executes the prepared query and iterates over the result dataset. - var res = setupQuery(db, stmtName, args) - var L = pqNfields(res) - var result = newRow(L) - for i in 0..pqNtuples(res)-1: - setRow(res, result, i, L) - yield result - pqClear(res) - -iterator instantRows*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): InstantRow - {.tags: [ReadDbEffect].} = - ## same as fastRows but returns a handle that can be used to get column text - ## on demand using []. Returned handle is valid only within iterator body. - var res = setupQuery(db, query, args) - for i in 0..pqNtuples(res)-1: - yield (res: res, line: i) - pqClear(res) - -iterator instantRows*(db: DbConn, stmtName: SqlPrepared, - args: varargs[string, `$`]): InstantRow - {.tags: [ReadDbEffect].} = - ## same as fastRows but returns a handle that can be used to get column text - ## on demand using []. Returned handle is valid only within iterator body. - var res = setupQuery(db, stmtName, args) - for i in 0..pqNtuples(res)-1: - yield (res: res, line: i) - pqClear(res) - -proc `[]`*(row: InstantRow, col: int32): string {.inline.} = - ## returns text for given column of the row - $pqgetvalue(row.res, row.line, col) - -proc len*(row: InstantRow): int32 {.inline.} = - ## returns number of columns in the row - pqNfields(row.res) - -proc getRow*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): Row {.tags: [ReadDbEffect].} = - ## retrieves a single row. If the query doesn't return any rows, this proc - ## will return a Row with empty strings for each column. - var res = setupQuery(db, query, args) - var L = pqnfields(res) - result = newRow(L) - setRow(res, result, 0, L) - pqclear(res) - -proc getRow*(db: DbConn, stmtName: SqlPrepared, - args: varargs[string, `$`]): Row {.tags: [ReadDbEffect].} = - var res = setupQuery(db, stmtName, args) - var L = pqNfields(res) - result = newRow(L) - setRow(res, result, 0, L) - pqClear(res) - -proc getAllRows*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): seq[Row] {. - tags: [ReadDbEffect].} = - ## executes the query and returns the whole result dataset. - result = @[] - for r in fastRows(db, query, args): - result.add(r) - -proc getAllRows*(db: DbConn, stmtName: SqlPrepared, - args: varargs[string, `$`]): seq[Row] {.tags: - [ReadDbEffect].} = - ## executes the prepared query and returns the whole result dataset. - result = @[] - for r in fastRows(db, stmtName, args): - result.add(r) - -iterator rows*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): Row {.tags: [ReadDbEffect].} = - ## same as `fastRows`, but slower and safe. - for r in items(getAllRows(db, query, args)): yield r - -iterator rows*(db: DbConn, stmtName: SqlPrepared, - args: varargs[string, `$`]): Row {.tags: [ReadDbEffect].} = - ## same as `fastRows`, but slower and safe. - for r in items(getAllRows(db, stmtName, args)): yield r - -proc getValue*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): string {. - tags: [ReadDbEffect].} = - ## executes the query and returns the first column of the first row of the - ## result dataset. Returns "" if the dataset contains no rows or the database - ## value is NULL. - var x = pqgetvalue(setupQuery(db, query, args), 0, 0) - result = if isNil(x): "" else: $x - -proc tryInsertID*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): int64 {. - tags: [WriteDbEffect].}= - ## executes the query (typically "INSERT") and returns the - ## generated ID for the row or -1 in case of an error. For Postgre this adds - ## ``RETURNING id`` to the query, so it only works if your primary key is - ## named ``id``. - var x = pqgetvalue(setupQuery(db, SqlQuery(string(query) & " RETURNING id"), - args), 0, 0) - if not isNil(x): - result = parseBiggestInt($x) - else: - result = -1 - -proc insertID*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): int64 {. - tags: [WriteDbEffect].} = - ## executes the query (typically "INSERT") and returns the - ## generated ID for the row. For Postgre this adds - ## ``RETURNING id`` to the query, so it only works if your primary key is - ## named ``id``. - result = tryInsertID(db, query, args) - if result < 0: dbError(db) - -proc execAffectedRows*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): int64 {.tags: [ - ReadDbEffect, WriteDbEffect].} = - ## executes the query (typically "UPDATE") and returns the - ## number of affected rows. - var q = dbFormat(query, args) - var res = pqExec(db, q) - if pqresultStatus(res) != PGRES_COMMAND_OK: dbError(db) - result = parseBiggestInt($pqcmdTuples(res)) - pqclear(res) - -proc execAffectedRows*(db: DbConn, stmtName: SqlPrepared, - args: varargs[string, `$`]): int64 {.tags: [ - ReadDbEffect, WriteDbEffect].} = - ## executes the query (typically "UPDATE") and returns the - ## number of affected rows. - var arr = allocCStringArray(args) - var res = pqexecPrepared(db, stmtName.string, int32(args.len), arr, - nil, nil, 0) - deallocCStringArray(arr) - if pqresultStatus(res) != PGRES_COMMAND_OK: dbError(db) - result = parseBiggestInt($pqcmdTuples(res)) - pqclear(res) - -proc close*(db: DbConn) {.tags: [DbEffect].} = - ## closes the database connection. - if db != nil: pqfinish(db) - -proc open*(connection, user, password, database: string): DbConn {. - tags: [DbEffect].} = - ## opens a database connection. Raises `EDb` if the connection could not - ## be established. - ## - ## Clients can also use Postgres keyword/value connection strings to - ## connect. - ## - ## Example: - ## - ## .. code-block:: nim - ## - ## con = open("", "", "", "host=localhost port=5432 dbname=mydb") - ## - ## See http://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING - ## for more information. - ## - ## Note that the connection parameter is not used but exists to maintain - ## the nim db api. - result = pqsetdbLogin(nil, nil, nil, nil, database, user, password) - if pqStatus(result) != CONNECTION_OK: dbError(result) # result = nil - -proc setEncoding*(connection: DbConn, encoding: string): bool {. - tags: [DbEffect].} = - ## sets the encoding of a database connection, returns true for - ## success, false for failure. - return pqsetClientEncoding(connection, encoding) == 0 - - -# Tests are in ../../tests/untestable/tpostgres. diff --git a/lib/impure/db_sqlite.nim b/lib/impure/db_sqlite.nim deleted file mode 100644 index 5cfed1eba..000000000 --- a/lib/impure/db_sqlite.nim +++ /dev/null @@ -1,291 +0,0 @@ -# -# -# Nim's Runtime Library -# (c) Copyright 2015 Andreas Rumpf -# -# See the file "copying.txt", included in this -# distribution, for details about the copyright. -# - -## A higher level `SQLite`:idx: database wrapper. This interface -## is implemented for other databases too. -## -## Example: -## -## .. code-block:: nim -## -## import db_sqlite, math -## -## let theDb = open("mytest.db", nil, nil, nil) -## -## theDb.exec(sql"Drop table if exists myTestTbl") -## theDb.exec(sql("""create table myTestTbl ( -## Id INTEGER PRIMARY KEY, -## Name VARCHAR(50) NOT NULL, -## i INT(11), -## f DECIMAL(18,10))""")) -## -## theDb.exec(sql"BEGIN") -## for i in 1..1000: -## theDb.exec(sql"INSERT INTO myTestTbl (name,i,f) VALUES (?,?,?)", -## "Item#" & $i, i, sqrt(i.float)) -## theDb.exec(sql"COMMIT") -## -## for x in theDb.fastRows(sql"select * from myTestTbl"): -## echo x -## -## let id = theDb.tryInsertId(sql"INSERT INTO myTestTbl (name,i,f) VALUES (?,?,?)", -## "Item#1001", 1001, sqrt(1001.0)) -## echo "Inserted item: ", theDb.getValue(sql"SELECT name FROM myTestTbl WHERE id=?", id) -## -## theDb.close() - -import strutils, sqlite3 - -import db_common -export db_common - -type - DbConn* = PSqlite3 ## encapsulates a database connection - Row* = seq[string] ## a row of a dataset. NULL database values will be - ## converted to nil. - InstantRow* = Pstmt ## a handle that can be used to get a row's column - ## text on demand -{.deprecated: [TRow: Row, TDbConn: DbConn].} - -proc dbError*(db: DbConn) {.noreturn.} = - ## raises a DbError exception. - var e: ref DbError - new(e) - e.msg = $sqlite3.errmsg(db) - raise e - -proc dbQuote*(s: string): string = - ## DB quotes the string. - if s.isNil: return "NULL" - result = "'" - for c in items(s): - if c == '\'': add(result, "''") - else: add(result, c) - add(result, '\'') - -proc dbFormat(formatstr: SqlQuery, args: varargs[string]): string = - result = "" - var a = 0 - for c in items(string(formatstr)): - if c == '?': - add(result, dbQuote(args[a])) - inc(a) - else: - add(result, c) - -proc tryExec*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): bool {. - tags: [ReadDbEffect, WriteDbEffect].} = - ## tries to execute the query and returns true if successful, false otherwise. - var q = dbFormat(query, args) - var stmt: sqlite3.Pstmt - if prepare_v2(db, q, q.len.cint, stmt, nil) == SQLITE_OK: - if step(stmt) == SQLITE_DONE: - result = finalize(stmt) == SQLITE_OK - -proc exec*(db: DbConn, query: SqlQuery, args: varargs[string, `$`]) {. - tags: [ReadDbEffect, WriteDbEffect].} = - ## executes the query and raises DbError if not successful. - if not tryExec(db, query, args): dbError(db) - -proc newRow(L: int): Row = - newSeq(result, L) - for i in 0..L-1: result[i] = "" - -proc setupQuery(db: DbConn, query: SqlQuery, - args: varargs[string]): Pstmt = - var q = dbFormat(query, args) - if prepare_v2(db, q, q.len.cint, result, nil) != SQLITE_OK: dbError(db) - -proc setRow(stmt: Pstmt, r: var Row, cols: cint) = - for col in 0..cols-1: - setLen(r[col], column_bytes(stmt, col)) # set capacity - setLen(r[col], 0) - let x = column_text(stmt, col) - if not isNil(x): add(r[col], x) - -iterator fastRows*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): Row {.tags: [ReadDbEffect].} = - ## Executes the query and iterates over the result dataset. - ## - ## This is very fast, but potentially dangerous. Use this iterator only - ## if you require **ALL** the rows. - ## - ## Breaking the fastRows() iterator during a loop will cause the next - ## database query to raise a DbError exception ``unable to close due to ...``. - var stmt = setupQuery(db, query, args) - var L = (column_count(stmt)) - var result = newRow(L) - while step(stmt) == SQLITE_ROW: - setRow(stmt, result, L) - yield result - if finalize(stmt) != SQLITE_OK: dbError(db) - -iterator instantRows*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): InstantRow - {.tags: [ReadDbEffect].} = - ## same as fastRows but returns a handle that can be used to get column text - ## on demand using []. Returned handle is valid only within the iterator body. - var stmt = setupQuery(db, query, args) - while step(stmt) == SQLITE_ROW: - yield stmt - if finalize(stmt) != SQLITE_OK: dbError(db) - -proc toTypeKind(t: var DbType; x: int32) = - case x - of SQLITE_INTEGER: - t.kind = dbInt - t.size = 8 - of SQLITE_FLOAT: - t.kind = dbFloat - t.size = 8 - of SQLITE_BLOB: t.kind = dbBlob - of SQLITE_NULL: t.kind = dbNull - of SQLITE_TEXT: t.kind = dbVarchar - else: t.kind = dbUnknown - -proc setColumns(columns: var DbColumns; x: PStmt) = - let L = column_count(x) - setLen(columns, L) - for i in 0'i32 ..< L: - columns[i].name = $column_name(x, i) - columns[i].typ.name = $column_decltype(x, i) - toTypeKind(columns[i].typ, column_type(x, i)) - columns[i].tableName = $column_table_name(x, i) - -iterator instantRows*(db: DbConn; columns: var DbColumns; query: SqlQuery, - args: varargs[string, `$`]): InstantRow - {.tags: [ReadDbEffect].} = - ## same as fastRows but returns a handle that can be used to get column text - ## on demand using []. Returned handle is valid only within the iterator body. - var stmt = setupQuery(db, query, args) - setColumns(columns, stmt) - while step(stmt) == SQLITE_ROW: - yield stmt - if finalize(stmt) != SQLITE_OK: dbError(db) - -proc `[]`*(row: InstantRow, col: int32): string {.inline.} = - ## returns text for given column of the row - $column_text(row, col) - -proc len*(row: InstantRow): int32 {.inline.} = - ## returns number of columns in the row - column_count(row) - -proc getRow*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): Row {.tags: [ReadDbEffect].} = - ## retrieves a single row. If the query doesn't return any rows, this proc - ## will return a Row with empty strings for each column. - var stmt = setupQuery(db, query, args) - var L = (column_count(stmt)) - result = newRow(L) - if step(stmt) == SQLITE_ROW: - setRow(stmt, result, L) - if finalize(stmt) != SQLITE_OK: dbError(db) - -proc getAllRows*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): seq[Row] {.tags: [ReadDbEffect].} = - ## executes the query and returns the whole result dataset. - result = @[] - for r in fastRows(db, query, args): - result.add(r) - -iterator rows*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): Row {.tags: [ReadDbEffect].} = - ## same as `FastRows`, but slower and safe. - for r in fastRows(db, query, args): yield r - -proc getValue*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): string {.tags: [ReadDbEffect].} = - ## executes the query and returns the first column of the first row of the - ## result dataset. Returns "" if the dataset contains no rows or the database - ## value is NULL. - var stmt = setupQuery(db, query, args) - if step(stmt) == SQLITE_ROW: - let cb = column_bytes(stmt, 0) - if cb == 0: - result = "" - else: - result = newStringOfCap(cb) - add(result, column_text(stmt, 0)) - else: - result = "" - if finalize(stmt) != SQLITE_OK: dbError(db) - -proc tryInsertID*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): int64 - {.tags: [WriteDbEffect], raises: [].} = - ## executes the query (typically "INSERT") and returns the - ## generated ID for the row or -1 in case of an error. - var q = dbFormat(query, args) - var stmt: sqlite3.Pstmt - result = -1 - if prepare_v2(db, q, q.len.cint, stmt, nil) == SQLITE_OK: - if step(stmt) == SQLITE_DONE: - result = last_insert_rowid(db) - if finalize(stmt) != SQLITE_OK: - result = -1 - -proc insertID*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): int64 {.tags: [WriteDbEffect].} = - ## executes the query (typically "INSERT") and returns the - ## generated ID for the row. For Postgre this adds - ## ``RETURNING id`` to the query, so it only works if your primary key is - ## named ``id``. - result = tryInsertID(db, query, args) - if result < 0: dbError(db) - -proc execAffectedRows*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): int64 {. - tags: [ReadDbEffect, WriteDbEffect].} = - ## executes the query (typically "UPDATE") and returns the - ## number of affected rows. - exec(db, query, args) - result = changes(db) - -proc close*(db: DbConn) {.tags: [DbEffect].} = - ## closes the database connection. - if sqlite3.close(db) != SQLITE_OK: dbError(db) - -proc open*(connection, user, password, database: string): DbConn {. - tags: [DbEffect].} = - ## opens a database connection. Raises `EDb` if the connection could not - ## be established. Only the ``connection`` parameter is used for ``sqlite``. - var db: DbConn - if sqlite3.open(connection, db) == SQLITE_OK: - result = db - else: - dbError(db) - -proc setEncoding*(connection: DbConn, encoding: string): bool {. - tags: [DbEffect].} = - ## sets the encoding of a database connection, returns true for - ## success, false for failure. - ## - ## Note that the encoding cannot be changed once it's been set. - ## According to SQLite3 documentation, any attempt to change - ## the encoding after the database is created will be silently - ## ignored. - exec(connection, sql"PRAGMA encoding = ?", [encoding]) - result = connection.getValue(sql"PRAGMA encoding") == encoding - -when not defined(testing) and isMainModule: - var db = open("db.sql", "", "", "") - exec(db, sql"create table tbl1(one varchar(10), two smallint)", []) - exec(db, sql"insert into tbl1 values('hello!',10)", []) - exec(db, sql"insert into tbl1 values('goodbye', 20)", []) - #db.query("create table tbl1(one varchar(10), two smallint)") - #db.query("insert into tbl1 values('hello!',10)") - #db.query("insert into tbl1 values('goodbye', 20)") - for r in db.rows(sql"select * from tbl1", []): - echo(r[0], r[1]) - for r in db.instantRows(sql"select * from tbl1", []): - echo(r[0], r[1]) - - db_sqlite.close(db) diff --git a/lib/impure/nre.nim b/lib/impure/nre.nim index 973f1f2ee..39d238055 100644 --- a/lib/impure/nre.nim +++ b/lib/impure/nre.nim @@ -1,87 +1,95 @@ # # Nim's Runtime Library -# (c) Copyright 2015 Nim Contributers +# (c) Copyright 2015 Nim Contributors # # See the file "copying.txt", included in this # distribution, for details about the copyright. # - -from pcre import nil -import nre.private.util -import tables -import unsigned -from strutils import toLower, `%` -from math import ceil -import options -from unicode import runeLenAt - +when defined(js): + {.error: "This library needs to be compiled with a c-like backend, and depends on PCRE; See jsre for JS backend.".} ## What is NRE? ## ============ ## ## A regular expression library for Nim using PCRE to do the hard work. ## +## For documentation on how to write patterns, there exists `the official PCRE +## pattern documentation +## <https://www.pcre.org/original/doc/html/pcrepattern.html>`_. You can also +## search the internet for a wide variety of third-party documentation and +## tools. +## +## .. warning:: If you love `sequtils.toSeq` we have bad news for you. This +## library doesn't work with it due to documented compiler limitations. As +## a workaround, use this: +runnableExamples: + # either `import std/nre except toSeq` or fully qualify `sequtils.toSeq`: + import std/sequtils + iterator iota(n: int): int = + for i in 0..<n: yield i + assert sequtils.toSeq(iota(3)) == @[0, 1, 2] +## .. note:: There are also alternative nimble packages such as [tinyre](https://github.com/khchen/tinyre) +## and [regex](https://github.com/nitely/nim-regex). ## Licencing ## --------- ## -## PCRE has some additional terms that you must comply with if you use this module.:: +## PCRE has `some additional terms`_ that you must agree to in order to use +## this module. ## -## > Copyright (c) 1997-2001 University of Cambridge -## > -## > Permission is granted to anyone to use this software for any purpose on any -## > computer system, and to redistribute it freely, subject to the following -## > restrictions: -## > -## > 1. This software is distributed in the hope that it will be useful, -## > but WITHOUT ANY WARRANTY; without even the implied warranty of -## > MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -## > -## > 2. The origin of this software must not be misrepresented, either by -## > explicit claim or by omission. In practice, this means that if you use -## > PCRE in software that you distribute to others, commercially or -## > otherwise, you must put a sentence like this -## > -## > Regular expression support is provided by the PCRE library package, -## > which is open source software, written by Philip Hazel, and copyright -## > by the University of Cambridge, England. -## > -## > somewhere reasonably visible in your documentation and in any relevant -## > files or online help data or similar. A reference to the ftp site for -## > the source, that is, to -## > -## > ftp://ftp.csx.cam.ac.uk/pub/software/programming/pcre/ -## > -## > should also be given in the documentation. However, this condition is not -## > intended to apply to whole chains of software. If package A includes PCRE, -## > it must acknowledge it, but if package B is software that includes package -## > A, the condition is not imposed on package B (unless it uses PCRE -## > independently). -## > -## > 3. Altered versions must be plainly marked as such, and must not be -## > misrepresented as being the original software. -## > -## > 4. If PCRE is embedded in any software that is released under the GNU -## > General Purpose Licence (GPL), or Lesser General Purpose Licence (LGPL), -## > then the terms of that licence shall supersede any condition above with -## > which it is incompatible. - - -# Type definitions {{{ +## .. _`some additional terms`: http://pcre.sourceforge.net/license.txt +runnableExamples: + import std/sugar + let vowels = re"[aeoui]" + let bounds = collect: + for match in "moiga".findIter(vowels): match.matchBounds + assert bounds == @[1 .. 1, 2 .. 2, 4 .. 4] + from std/sequtils import toSeq + let s = sequtils.toSeq("moiga".findIter(vowels)) + # fully qualified to avoid confusion with nre.toSeq + assert s.len == 3 + + let firstVowel = "foo".find(vowels) + let hasVowel = firstVowel.isSome() + assert hasVowel + let matchBounds = firstVowel.get().captureBounds[-1] + assert matchBounds.a == 1 + + # as with module `re`, unless specified otherwise, `start` parameter in each + # proc indicates where the scan starts, but outputs are relative to the start + # of the input string, not to `start`: + assert find("uxabc", re"(?<=x|y)ab", start = 1).get.captures[-1] == "ab" + assert find("uxabc", re"ab", start = 3).isNone + +from std/pcre import nil +import nre/private/util +import std/tables +from std/strutils import `%` +import std/options +from std/unicode import runeLenAt + +when defined(nimPreviewSlimSystem): + import std/assertions + +export options + type Regex* = ref object ## Represents the pattern that things are matched against, constructed with - ## ``re(string)``. Examples: ``re"foo"``, ``re(r"(*ANYCRLF)(?x)foo # - ## comment".`` + ## `re(string)`. Examples: `re"foo"`, `re(r"(*ANYCRLF)(?x)foo # + ## comment".` ## - ## ``pattern: string`` - ## the string that was used to create the pattern. + ## `pattern: string` + ## : the string that was used to create the pattern. For details on how + ## to write a pattern, please see `the official PCRE pattern + ## documentation. + ## <https://www.pcre.org/original/doc/html/pcrepattern.html>`_ ## - ## ``captureCount: int`` - ## the number of captures that the pattern has. + ## `captureCount: int` + ## : the number of captures that the pattern has. ## - ## ``captureNameId: Table[string, int]`` - ## a table from the capture names to their numeric id. + ## `captureNameId: Table[string, int]` + ## : a table from the capture names to their numeric id. ## ## ## Options @@ -90,30 +98,30 @@ type ## The following options may appear anywhere in the pattern, and they affect ## the rest of it. ## - ## - ``(?i)`` - case insensitive - ## - ``(?m)`` - multi-line: ``^`` and ``$`` match the beginning and end of + ## - `(?i)` - case insensitive + ## - `(?m)` - multi-line: `^` and `$` match the beginning and end of ## lines, not of the subject string - ## - ``(?s)`` - ``.`` also matches newline (*dotall*) - ## - ``(?U)`` - expressions are not greedy by default. ``?`` can be added + ## - `(?s)` - `.` also matches newline (*dotall*) + ## - `(?U)` - expressions are not greedy by default. `?` can be added ## to a qualifier to make it greedy - ## - ``(?x)`` - whitespace and comments (``#``) are ignored (*extended*) - ## - ``(?X)`` - character escapes without special meaning (``\w`` vs. - ## ``\a``) are errors (*extra*) + ## - `(?x)` - whitespace and comments (`#`) are ignored (*extended*) + ## - `(?X)` - character escapes without special meaning (`\w` vs. + ## `\a`) are errors (*extra*) ## ## One or a combination of these options may appear only at the beginning ## of the pattern: ## - ## - ``(*UTF8)`` - treat both the pattern and subject as UTF-8 - ## - ``(*UCP)`` - Unicode character properties; ``\w`` matches ``я`` - ## - ``(*U)`` - a combination of the two options above - ## - ``(*FIRSTLINE*)`` - fails if there is not a match on the first line - ## - ``(*NO_AUTO_CAPTURE)`` - turn off auto-capture for groups; - ## ``(?<name>...)`` can be used to capture - ## - ``(*CR)`` - newlines are separated by ``\r`` - ## - ``(*LF)`` - newlines are separated by ``\n`` (UNIX default) - ## - ``(*CRLF)`` - newlines are separated by ``\r\n`` (Windows default) - ## - ``(*ANYCRLF)`` - newlines are separated by any of the above - ## - ``(*ANY)`` - newlines are separated by any of the above and Unicode + ## - `(*UTF8)` - treat both the pattern and subject as UTF-8 + ## - `(*UCP)` - Unicode character properties; `\w` matches `я` + ## - `(*U)` - a combination of the two options above + ## - `(*FIRSTLINE*)` - fails if there is not a match on the first line + ## - `(*NO_AUTO_CAPTURE)` - turn off auto-capture for groups; + ## `(?<name>...)` can be used to capture + ## - `(*CR)` - newlines are separated by `\r` + ## - `(*LF)` - newlines are separated by `\n` (UNIX default) + ## - `(*CRLF)` - newlines are separated by `\r\n` (Windows default) + ## - `(*ANYCRLF)` - newlines are separated by any of the above + ## - `(*ANY)` - newlines are separated by any of the above and Unicode ## newlines: ## ## single characters VT (vertical tab, U+000B), FF (form feed, U+000C), @@ -122,16 +130,22 @@ type ## are recognized only in UTF-8 mode. ## — man pcre ## - ## - ``(*JAVASCRIPT_COMPAT)`` - JavaScript compatibility - ## - ``(*NO_STUDY)`` - turn off studying; study is enabled by default + ## - `(*JAVASCRIPT_COMPAT)` - JavaScript compatibility + ## - `(*NO_STUDY)` - turn off studying; study is enabled by default ## ## For more details on the leading option groups, see the `Option - ## Setting <http://man7.org/linux/man-pages/man3/pcresyntax.3.html#OPTION_SETTING>`__ + ## Setting <http://man7.org/linux/man-pages/man3/pcresyntax.3.html#OPTION_SETTING>`_ ## and the `Newline - ## Convention <http://man7.org/linux/man-pages/man3/pcresyntax.3.html#NEWLINE_CONVENTION>`__ + ## Convention <http://man7.org/linux/man-pages/man3/pcresyntax.3.html#NEWLINE_CONVENTION>`_ ## sections of the `PCRE syntax - ## manual <http://man7.org/linux/man-pages/man3/pcresyntax.3.html>`__. - pattern*: string ## not nil + ## manual <http://man7.org/linux/man-pages/man3/pcresyntax.3.html>`_. + ## + ## Some of these options are not part of PCRE and are converted by nre + ## into PCRE flags. These include `NEVER_UTF`, `ANCHORED`, + ## `DOLLAR_ENDONLY`, `FIRSTLINE`, `NO_AUTO_CAPTURE`, + ## `JAVASCRIPT_COMPAT`, `U`, `NO_STUDY`. In other PCRE wrappers, you + ## will need to pass these as separate flags to PCRE. + pattern*: string pcreObj: ptr pcre.Pcre ## not nil pcreExtra: ptr pcre.ExtraData ## nil @@ -141,57 +155,48 @@ type ## Usually seen as Option[RegexMatch], it represents the result of an ## execution. On failure, it is none, on success, it is some. ## - ## ``pattern: Regex`` - ## the pattern that is being matched + ## `pattern: Regex` + ## : the pattern that is being matched ## - ## ``str: string`` - ## the string that was matched against + ## `str: string` + ## : the string that was matched against ## - ## ``captures[]: string`` - ## the string value of whatever was captured at that id. If the value - ## is invalid, then behavior is undefined. If the id is ``-1``, then + ## `captures[]: string` + ## : the string value of whatever was captured at that id. If the value + ## is invalid, then behavior is undefined. If the id is `-1`, then ## the whole match is returned. If the given capture was not matched, - ## ``nil`` is returned. + ## `nil` is returned. See examples for `match`. ## - ## - ``"abc".match(re"(\w)").captures[0] == "a"`` - ## - ``"abc".match(re"(?<letter>\w)").captures["letter"] == "a"`` - ## - ``"abc".match(re"(\w)\w").captures[-1] == "ab"`` + ## `captureBounds[]: HSlice[int, int]` + ## : gets the bounds of the given capture according to the same rules as + ## the above. If the capture is not filled, then `None` is returned. + ## The bounds are both inclusive. See examples for `match`. ## - ## ``captureBounds[]: Option[Slice[int]]`` - ## gets the bounds of the given capture according to the same rules as - ## the above. If the capture is not filled, then ``None`` is returned. - ## The bounds are both inclusive. + ## `match: string` + ## : the full text of the match. ## - ## - ``"abc".match(re"(\w)").captureBounds[0] == 0 .. 0`` - ## - ``"abc".match(re"").captureBounds[-1] == 0 .. -1`` - ## - ``"abc".match(re"abc").captureBounds[-1] == 0 .. 2`` + ## `matchBounds: HSlice[int, int]` + ## : the bounds of the match, as in `captureBounds[]` ## - ## ``match: string`` - ## the full text of the match. + ## `(captureBounds|captures).toTable` + ## : returns a table with each named capture as a key. ## - ## ``matchBounds: Slice[int]`` - ## the bounds of the match, as in ``captureBounds[]`` + ## `(captureBounds|captures).toSeq` + ## : returns all the captures by their number. ## - ## ``(captureBounds|captures).toTable`` - ## returns a table with each named capture as a key. - ## - ## ``(captureBounds|captures).toSeq`` - ## returns all the captures by their number. - ## - ## ``$: string`` - ## same as ``match`` + ## `$: string` + ## : same as `match` pattern*: Regex ## The regex doing the matching. ## Not nil. str*: string ## The string that was matched against. - ## Not nil. - pcreMatchBounds: seq[Slice[cint]] ## First item is the bounds of the match - ## Other items are the captures - ## `a` is inclusive start, `b` is exclusive end + pcreMatchBounds: seq[HSlice[cint, cint]] ## First item is the bounds of the match + ## Other items are the captures + ## `a` is inclusive start, `b` is exclusive end Captures* = distinct RegexMatch CaptureBounds* = distinct RegexMatch - RegexError* = ref object of Exception + RegexError* = ref object of CatchableError RegexInternalError* = ref object of RegexError ## Internal error in the module, this probably means that there is a bug @@ -207,19 +212,71 @@ type pattern*: string ## the pattern that caused the problem StudyError* = ref object of RegexError - ## Thrown when studying the regular expression failes + ## Thrown when studying the regular expression fails ## for whatever reason. The message contains the error ## code. -# }}} + +proc destroyRegex(pattern: Regex) = + `=destroy`(pattern.pattern) + pcre.free_substring(cast[cstring](pattern.pcreObj)) + if pattern.pcreExtra != nil: + pcre.free_study(pattern.pcreExtra) + `=destroy`(pattern.captureNameToId) proc getinfo[T](pattern: Regex, opt: cint): T = let retcode = pcre.fullinfo(pattern.pcreObj, pattern.pcreExtra, opt, addr result) if retcode < 0: # XXX Error message that doesn't expose implementation details - raise newException(FieldError, "Invalid getinfo for $1, errno $2" % [$opt, $retcode]) + raise newException(FieldDefect, "Invalid getinfo for $1, errno $2" % [$opt, $retcode]) + +proc getNameToNumberTable(pattern: Regex): Table[string, int] = + let entryCount = getinfo[cint](pattern, pcre.INFO_NAMECOUNT) + let entrySize = getinfo[cint](pattern, pcre.INFO_NAMEENTRYSIZE) + let table = cast[ptr UncheckedArray[uint8]]( + getinfo[int](pattern, pcre.INFO_NAMETABLE)) + + result = initTable[string, int]() + + for i in 0 ..< entryCount: + let pos = i * entrySize + let num = (int(table[pos]) shl 8) or int(table[pos + 1]) - 1 + var name = "" + + var idx = 2 + while table[pos + idx] != 0: + name.add(char(table[pos + idx])) + idx += 1 + + result[name] = num + +proc initRegex(pattern: string, flags: int, study = true): Regex = + new(result, destroyRegex) + result.pattern = pattern + + var errorMsg: cstring + var errOffset: cint + + result.pcreObj = pcre.compile(cstring(pattern), + # better hope int is at least 4 bytes.. + cint(flags), addr errorMsg, + addr errOffset, nil) + if result.pcreObj == nil: + # failed to compile + raise SyntaxError(msg: $errorMsg, pos: errOffset, pattern: pattern) + + if study: + var options: cint = 0 + var hasJit: cint + if pcre.config(pcre.CONFIG_JIT, addr hasJit) == 0: + if hasJit == 1'i32: + options = pcre.STUDY_JIT_COMPILE + result.pcreExtra = pcre.study(result.pcreObj, options, addr errorMsg) + if errorMsg != nil: + raise StudyError(msg: $errorMsg) + + result.captureNameToId = result.getNameToNumberTable() -# Regex accessors {{{ proc captureCount*(pattern: Regex): int = return getinfo[cint](pattern, pcre.INFO_CAPTURECOUNT) @@ -231,7 +288,7 @@ proc matchesCrLf(pattern: Regex): bool = let newlineFlags = flags and (pcre.NEWLINE_CRLF or pcre.NEWLINE_ANY or pcre.NEWLINE_ANYCRLF) - if newLineFlags > 0u32: + if newlineFlags > 0u32: return true # get flags from build config @@ -246,101 +303,122 @@ proc matchesCrLf(pattern: Regex): bool = of -2: return true of -1: return true else: return false -# }}} -# Capture accessors {{{ -proc captureBounds*(pattern: RegexMatch): CaptureBounds = return CaptureBounds(pattern) -proc captures*(pattern: RegexMatch): Captures = return Captures(pattern) +func captureBounds*(pattern: RegexMatch): CaptureBounds = return CaptureBounds(pattern) + +func captures*(pattern: RegexMatch): Captures = return Captures(pattern) -proc `[]`*(pattern: CaptureBounds, i: int): Option[Slice[int]] = +func contains*(pattern: CaptureBounds, i: int): bool = let pattern = RegexMatch(pattern) - if pattern.pcreMatchBounds[i + 1].a != -1: - let bounds = pattern.pcreMatchBounds[i + 1] - return some(int(bounds.a) .. int(bounds.b-1)) - else: - return none(Slice[int]) + pattern.pcreMatchBounds[i + 1].a != -1 + +func contains*(pattern: Captures, i: int): bool = + i in CaptureBounds(pattern) -proc `[]`*(pattern: Captures, i: int): string = +func `[]`*(pattern: CaptureBounds, i: int): HSlice[int, int] = + let pattern = RegexMatch(pattern) + if not (i in pattern.captureBounds): + raise newException(IndexDefect, "Group '" & $i & "' was not captured") + + let bounds = pattern.pcreMatchBounds[i + 1] + int(bounds.a)..int(bounds.b-1) + +func `[]`*(pattern: Captures, i: int): string = let pattern = RegexMatch(pattern) let bounds = pattern.captureBounds[i] - if bounds.isSome: - let bounds = bounds.get - return pattern.str.substr(bounds.a, bounds.b) - else: - return nil + pattern.str.substr(bounds.a, bounds.b) -proc match*(pattern: RegexMatch): string = +func match*(pattern: RegexMatch): string = return pattern.captures[-1] -proc matchBounds*(pattern: RegexMatch): Slice[int] = - return pattern.captureBounds[-1].get +func matchBounds*(pattern: RegexMatch): HSlice[int, int] = + return pattern.captureBounds[-1] -proc `[]`*(pattern: CaptureBounds, name: string): Option[Slice[int]] = +func contains*(pattern: CaptureBounds, name: string): bool = let pattern = RegexMatch(pattern) - return pattern.captureBounds[pattern.pattern.captureNameToId.fget(name)] + let nameToId = pattern.pattern.captureNameToId + if not (name in nameToId): + return false + nameToId[name] in pattern.captureBounds -proc `[]`*(pattern: Captures, name: string): string = +func contains*(pattern: Captures, name: string): bool = + name in CaptureBounds(pattern) + +func checkNamedCaptured(pattern: RegexMatch, name: string) = + if not (name in pattern.captureBounds): + raise newException(KeyError, "Group '" & name & "' was not captured") + +func `[]`*(pattern: CaptureBounds, name: string): HSlice[int, int] = + let pattern = RegexMatch(pattern) + checkNamedCaptured(pattern, name) + {.noSideEffect.}: + result = pattern.captureBounds[pattern.pattern.captureNameToId[name]] + +func `[]`*(pattern: Captures, name: string): string = let pattern = RegexMatch(pattern) - return pattern.captures[pattern.pattern.captureNameToId.fget(name)] + checkNamedCaptured(pattern, name) + {.noSideEffect.}: + result = pattern.captures[pattern.pattern.captureNameToId[name]] -template toTableImpl(cond: bool): stmt {.immediate, dirty.} = +template toTableImpl() {.dirty.} = for key in RegexMatch(pattern).pattern.captureNameId.keys: - let nextVal = pattern[key] - if cond: - result[key] = default - else: - result[key] = nextVal + if key in pattern: + result[key] = pattern[key] -proc toTable*(pattern: Captures, default: string = nil): Table[string, string] = +func toTable*(pattern: Captures): Table[string, string] = result = initTable[string, string]() - toTableImpl(nextVal == nil) + toTableImpl() -proc toTable*(pattern: CaptureBounds, default = none(Slice[int])): - Table[string, Option[Slice[int]]] = - result = initTable[string, Option[Slice[int]]]() - toTableImpl(nextVal.isNone) +func toTable*(pattern: CaptureBounds): Table[string, HSlice[int, int]] = + result = initTable[string, HSlice[int, int]]() + toTableImpl() -template itemsImpl(cond: bool): stmt {.immediate, dirty.} = - for i in 0 .. <RegexMatch(pattern).pattern.captureCount: - let nextVal = pattern[i] +template itemsImpl() {.dirty.} = + for i in 0 ..< RegexMatch(pattern).pattern.captureCount: # done in this roundabout way to avoid multiple yields (potential code # bloat) - let nextYieldVal = if cond: default else: nextVal - yield nextYieldVal + let nextYieldVal = if i in pattern: + some(pattern[i]) + else: + default + yield nextYieldVal -iterator items*(pattern: CaptureBounds, default = none(Slice[int])): Option[Slice[int]] = - itemsImpl(nextVal.isNone) +iterator items*(pattern: CaptureBounds, + default = none(HSlice[int, int])): Option[HSlice[int, int]] = + itemsImpl() -iterator items*(pattern: Captures, default: string = nil): string = - itemsImpl(nextVal == nil) +iterator items*(pattern: Captures, + default: Option[string] = none(string)): Option[string] = + itemsImpl() -proc toSeq*(pattern: CaptureBounds, default = none(Slice[int])): seq[Option[Slice[int]]] = - accumulateResult(pattern.items(default)) +proc toSeq*(pattern: CaptureBounds, + default = none(HSlice[int, int])): seq[Option[HSlice[int, int]]] = + result = @[] + for it in pattern.items(default): result.add it -proc toSeq*(pattern: Captures, default: string = nil): seq[string] = - accumulateResult(pattern.items(default)) +proc toSeq*(pattern: Captures, + default: Option[string] = none(string)): seq[Option[string]] = + result = @[] + for it in pattern.items(default): result.add it proc `$`*(pattern: RegexMatch): string = return pattern.captures[-1] proc `==`*(a, b: Regex): bool = if not a.isNil and not b.isNil: - return a.pattern == b.pattern and - a.pcreObj == b.pcreObj and + return a.pattern == b.pattern and + a.pcreObj == b.pcreObj and a.pcreExtra == b.pcreExtra else: return system.`==`(a, b) proc `==`*(a, b: RegexMatch): bool = return a.pattern == b.pattern and - a.str == b.str -# }}} + a.str == b.str -# Creation & Destruction {{{ -# PCRE Options {{{ const PcreOptions = { "NEVER_UTF": pcre.NEVER_UTF, "ANCHORED": pcre.ANCHORED, @@ -396,73 +474,19 @@ proc extractOptions(pattern: string): tuple[pattern: string, flags: int, study: result.pattern.add pattern[optionStart .. pattern.high] -# }}} - -type UncheckedArray {.unchecked.}[T] = array[0 .. 0, T] - -proc destroyRegex(pattern: Regex) = - pcre.free_substring(cast[cstring](pattern.pcreObj)) - pattern.pcreObj = nil - if pattern.pcreExtra != nil: - pcre.free_study(pattern.pcreExtra) - -proc getNameToNumberTable(pattern: Regex): Table[string, int] = - let entryCount = getinfo[cint](pattern, pcre.INFO_NAMECOUNT) - let entrySize = getinfo[cint](pattern, pcre.INFO_NAMEENTRYSIZE) - let table = cast[ptr UncheckedArray[uint8]]( - getinfo[int](pattern, pcre.INFO_NAMETABLE)) - - result = initTable[string, int]() - - for i in 0 .. <entryCount: - let pos = i * entrySize - let num = (int(table[pos]) shl 8) or int(table[pos + 1]) - 1 - var name = "" - - var idx = 2 - while table[pos + idx] != 0: - name.add(char(table[pos + idx])) - idx += 1 - - result[name] = num - -proc initRegex(pattern: string, flags: int, study = true): Regex = - new(result, destroyRegex) - result.pattern = pattern - - var errorMsg: cstring - var errOffset: cint - - result.pcreObj = pcre.compile(cstring(pattern), - # better hope int is at least 4 bytes.. - cint(flags), addr errorMsg, - addr errOffset, nil) - if result.pcreObj == nil: - # failed to compile - raise SyntaxError(msg: $errorMsg, pos: errOffset, pattern: pattern) - - if study: - # XXX investigate JIT - result.pcreExtra = pcre.study(result.pcreObj, 0x0, addr errorMsg) - if errorMsg != nil: - raise StudyError(msg: $errorMsg) - - result.captureNameToId = result.getNameToNumberTable() - proc re*(pattern: string): Regex = let (pattern, flags, study) = extractOptions(pattern) initRegex(pattern, flags, study) -# }}} -# Operations {{{ proc matchImpl(str: string, pattern: Regex, start, endpos: int, flags: int): Option[RegexMatch] = - var myResult = RegexMatch(pattern : pattern, str : str) + var myResult = RegexMatch(pattern: pattern, str: str) # See PCRE man pages. # 2x capture count to make room for start-end pairs # 1x capture count as slack space for PCRE let vecsize = (pattern.captureCount() + 1) * 3 # div 2 because each element is 2 cints long - myResult.pcreMatchBounds = newSeq[Slice[cint]](ceil(vecsize / 2).int) + # plus 1 because we need the ceiling, not the floor + myResult.pcreMatchBounds = newSeq[HSlice[cint, cint]]((vecsize + 1) div 2) myResult.pcreMatchBounds.setLen(vecsize div 3) let strlen = if endpos == int.high: str.len else: endpos+1 @@ -483,54 +507,67 @@ proc matchImpl(str: string, pattern: Regex, start, endpos: int, flags: int): Opt of pcre.ERROR_NOMATCH: return none(RegexMatch) of pcre.ERROR_NULL: - raise newException(AccessViolationError, "Expected non-null parameters") + raise newException(AccessViolationDefect, "Expected non-null parameters") of pcre.ERROR_BADOPTION: - raise RegexInternalError(msg : "Unknown pattern flag. Either a bug or " & + raise RegexInternalError(msg: "Unknown pattern flag. Either a bug or " & "outdated PCRE.") of pcre.ERROR_BADUTF8, pcre.ERROR_SHORTUTF8, pcre.ERROR_BADUTF8_OFFSET: - raise InvalidUnicodeError(msg : "Invalid unicode byte sequence", - pos : myResult.pcreMatchBounds[0].a) + raise InvalidUnicodeError(msg: "Invalid unicode byte sequence", + pos: myResult.pcreMatchBounds[0].a) else: - raise RegexInternalError(msg : "Unknown internal error: " & $execRet) + raise RegexInternalError(msg: "Unknown internal error: " & $execRet) proc match*(str: string, pattern: Regex, start = 0, endpos = int.high): Option[RegexMatch] = - ## Like ```find(...)`` <#proc-find>`__, but anchored to the start of the - ## string. This means that ``"foo".match(re"f") == true``, but - ## ``"foo".match(re"o") == false``. + ## Like `find(...)<#find,string,Regex,int>`_, but anchored to the start of the + ## string. + runnableExamples: + assert "foo".match(re"f").isSome + assert "foo".match(re"o").isNone + + assert "abc".match(re"(\w)").get.captures[0] == "a" + assert "abc".match(re"(?<letter>\w)").get.captures["letter"] == "a" + assert "abc".match(re"(\w)\w").get.captures[-1] == "ab" + + assert "abc".match(re"(\w)").get.captureBounds[0] == 0 .. 0 + assert 0 in "abc".match(re"(\w)").get.captureBounds + assert "abc".match(re"").get.captureBounds[-1] == 0 .. -1 + assert "abc".match(re"abc").get.captureBounds[-1] == 0 .. 2 return str.matchImpl(pattern, start, endpos, pcre.ANCHORED) iterator findIter*(str: string, pattern: Regex, start = 0, endpos = int.high): RegexMatch = - ## Works the same as ```find(...)`` <#proc-find>`__, but finds every - ## non-overlapping match. ``"2222".find(re"22")`` is ``"22", "22"``, not - ## ``"22", "22", "22"``. - ## - ## Arguments are the same as ```find(...)`` <#proc-find>`__ + ## Works the same as `find(...)<#find,string,Regex,int>`_, but finds every + ## non-overlapping match: + runnableExamples: + import std/sugar + assert collect(for a in "2222".findIter(re"22"): a.match) == @["22", "22"] + # not @["22", "22", "22"] + ## Arguments are the same as `find(...)<#find,string,Regex,int>`_ ## ## Variants: ## - ## - ``proc findAll(...)`` returns a ``seq[string]`` - # see pcredemo for explaination + ## - `proc findAll(...)` returns a `seq[string]` + # see pcredemo for explanation => https://www.pcre.org/original/doc/html/pcredemo.html let matchesCrLf = pattern.matchesCrLf() let unicode = uint32(getinfo[culong](pattern, pcre.INFO_OPTIONS) and pcre.UTF8) > 0u32 let strlen = if endpos == int.high: str.len else: endpos+1 - var offset = start var match: Option[RegexMatch] + var neverMatched = true + while true: var flags = 0 - if match.isSome and match.get.matchBounds.a > match.get.matchBounds.b: # 0-len match flags = pcre.NOTEMPTY_ATSTART - match = str.matchImpl(pattern, offset, endpos, flags) if match.isNone: # either the end of the input or the string - # cannot be split here - if offset >= strlen: + # cannot be split here - we also need to bail + # if we've never matched and we've already tried to... + if flags == 0 or offset >= strlen or neverMatched: # All matches found break if matchesCrLf and offset < (str.len - 1) and @@ -544,21 +581,20 @@ iterator findIter*(str: string, pattern: Regex, start = 0, endpos = int.high): R else: offset += 1 else: + neverMatched = false offset = match.get.matchBounds.b + 1 - yield match.get - proc find*(str: string, pattern: Regex, start = 0, endpos = int.high): Option[RegexMatch] = ## Finds the given pattern in the string between the end and start ## positions. ## - ## ``start`` - ## The start point at which to start matching. ``|abc`` is ``0``; - ## ``a|bc`` is ``1`` + ## `start` + ## : The start point at which to start matching. `|abc` is `0`; + ## `a|bc` is `1` ## - ## ``endpos`` - ## The maximum index for a match; ``int.high`` means the end of the + ## `endpos` + ## : The maximum index for a match; `int.high` means the end of the ## string, otherwise it’s an inclusive upper bound. return str.matchImpl(pattern, start, endpos, 0) @@ -567,22 +603,36 @@ proc findAll*(str: string, pattern: Regex, start = 0, endpos = int.high): seq[st for match in str.findIter(pattern, start, endpos): result.add(match.match) +proc contains*(str: string, pattern: Regex, start = 0, endpos = int.high): bool = + ## Determine if the string contains the given pattern between the end and + ## start positions: + ## This function is equivalent to `isSome(str.find(pattern, start, endpos))`. + runnableExamples: + assert "abc".contains(re"bc") + assert not "abc".contains(re"cd") + assert not "abc".contains(re"a", start = 1) + + return isSome(str.find(pattern, start, endpos)) + proc split*(str: string, pattern: Regex, maxSplit = -1, start = 0): seq[string] = ## Splits the string with the given regex. This works according to the - ## rules that Perl and Javascript use: + ## rules that Perl and Javascript use. ## - ## - If the match is zero-width, then the string is still split: - ## ``"123".split(r"") == @["1", "2", "3"]``. + ## `start` behaves the same as in `find(...)<#find,string,Regex,int>`_. ## - ## - If the pattern has a capture in it, it is added after the string - ## split: ``"12".split(re"(\d)") == @["", "1", "", "2", ""]``. - ## - ## - If ``maxsplit != -1``, then the string will only be split - ## ``maxsplit - 1`` times. This means that there will be ``maxsplit`` - ## strings in the output seq. - ## ``"1.2.3".split(re"\.", maxsplit = 2) == @["1", "2.3"]`` - ## - ## ``start`` behaves the same as in ```find(...)`` <#proc-find>`__. + runnableExamples: + # - If the match is zero-width, then the string is still split: + assert "123".split(re"") == @["1", "2", "3"] + + # - If the pattern has a capture in it, it is added after the string + # split: + assert "12".split(re"(\d)") == @["", "1", "", "2", ""] + + # - If `maxsplit != -1`, then the string will only be split + # `maxsplit - 1` times. This means that there will be `maxsplit` + # strings in the output seq. + assert "1.2.3".split(re"\.", maxsplit = 2) == @["1", "2.3"] + result = @[] var lastIdx = start var splits = 0 @@ -610,7 +660,8 @@ proc split*(str: string, pattern: Regex, maxSplit = -1, start = 0): seq[string] for cap in match.captures: # if there are captures, include them in the result - result.add(cap) + if cap.isSome: + result.add(cap.get) if splits == maxSplit - 1: break @@ -626,7 +677,7 @@ proc split*(str: string, pattern: Regex, maxSplit = -1, start = 0): seq[string] result.add(str.substr(bounds.b + 1, str.high)) template replaceImpl(str: string, pattern: Regex, - replacement: expr): stmt {.immediate, dirty.} = + replacement: untyped) {.dirty.} = # XXX seems very similar to split, maybe I can reduce code duplication # somehow? result = "" @@ -635,7 +686,6 @@ template replaceImpl(str: string, pattern: Regex, let bounds = match.matchBounds result.add(str.substr(lastIdx, bounds.a - 1)) let nextVal = replacement - assert(nextVal != nil) result.add(nextVal) lastIdx = bounds.b + 1 @@ -645,27 +695,27 @@ template replaceImpl(str: string, pattern: Regex, proc replace*(str: string, pattern: Regex, subproc: proc (match: RegexMatch): string): string = - ## Replaces each match of Regex in the string with ``sub``, which should - ## never be or return ``nil``. + ## Replaces each match of Regex in the string with `subproc`, which should + ## never be or return `nil`. ## - ## If ``sub`` is a ``proc (RegexMatch): string``, then it is executed with + ## If `subproc` is a `proc (RegexMatch): string`, then it is executed with ## each match and the return value is the replacement value. ## - ## If ``sub`` is a ``proc (string): string``, then it is executed with the - ## full text of the match and and the return value is the replacement - ## value. + ## If `subproc` is a `proc (string): string`, then it is executed with the + ## full text of the match and the return value is the replacement value. ## - ## If ``sub`` is a string, the syntax is as follows: + ## If `subproc` is a string, the syntax is as follows: ## - ## - ``$$`` - literal ``$`` - ## - ``$123`` - capture number ``123`` - ## - ``$foo`` - named capture ``foo`` - ## - ``${foo}`` - same as above - ## - ``$1$#`` - first and second captures - ## - ``$#`` - first capture - ## - ``$0`` - full match + ## - `$$` - literal `$` + ## - `$123` - capture number `123` + ## - `$foo` - named capture `foo` + ## - `${foo}` - same as above + ## - `$1$#` - first and second captures + ## - `$#` - first capture + ## - `$0` - full match ## - ## If a given capture is missing, a ``ValueError`` exception is thrown. + ## If a given capture is missing, `IndexDefect` thrown for un-named captures + ## and `KeyError` for named captures. replaceImpl(str, pattern, subproc(match)) proc replace*(str: string, pattern: Regex, @@ -677,10 +727,25 @@ proc replace*(str: string, pattern: Regex, sub: string): string = replaceImpl(str, pattern, formatStr(sub, match.captures[name], match.captures[id - 1])) -# }}} - -let SpecialCharMatcher = re"([\\+*?[^\]$(){}=!<>|:-])" -proc escapeRe*(str: string): string = - ## Escapes the string so it doesn’t match any special characters. - ## Incompatible with the Extra flag (``X``). - str.replace(SpecialCharMatcher, "\\$1") +proc escapeRe*(str: string): string {.gcsafe.} = + ## Escapes the string so it doesn't match any special characters. + ## Incompatible with the Extra flag (`X`). + ## + ## Escaped char: `\ + * ? [ ^ ] $ ( ) { } = ! < > | : -` + runnableExamples: + assert escapeRe("fly+wind") == "fly\\+wind" + assert escapeRe("!") == "\\!" + assert escapeRe("nim*") == "nim\\*" + + #([\\+*?[^\]$(){}=!<>|:-]) + const SpecialCharMatcher = {'\\', '+', '*', '?', '[', '^', ']', '$', '(', + ')', '{', '}', '=', '!', '<', '>', '|', ':', + '-'} + + for c in items(str): + case c + of SpecialCharMatcher: + result.add("\\") + result.add(c) + else: + result.add(c) diff --git a/lib/impure/nre/private/util.nim b/lib/impure/nre/private/util.nim index 253bfada7..ed8420776 100644 --- a/lib/impure/nre/private/util.nim +++ b/lib/impure/nre/private/util.nim @@ -1,22 +1,10 @@ ## INTERNAL FILE FOR USE ONLY BY nre.nim. -import tables - -proc fget*[K, V](self: Table[K, V], key: K): V = - if self.hasKey(key): - return self[key] - else: - raise newException(KeyError, "Key does not exist in table: " & $key) +import std/tables const Ident = {'a'..'z', 'A'..'Z', '0'..'9', '_', '\128'..'\255'} const StartIdent = Ident - {'0'..'9'} -proc checkNil(arg: string): string = - if arg == nil: - raise newException(ValueError, "Cannot use nil capture") - else: - return arg - -template formatStr*(howExpr, namegetter, idgetter: expr): expr = +template formatStr*(howExpr, namegetter, idgetter): untyped = let how = howExpr var val = newStringOfCap(how.len) var i = 0 @@ -32,7 +20,7 @@ template formatStr*(howExpr, namegetter, idgetter: expr): expr = i += 2 elif how[i + 1] == '#': var id {.inject.} = lastNum - val.add(checkNil(idgetter)) + val.add(idgetter) lastNum += 1 i += 2 elif how[i + 1] in {'0'..'9'}: @@ -41,7 +29,7 @@ template formatStr*(howExpr, namegetter, idgetter: expr): expr = while i < how.len and how[i] in {'0'..'9'}: id += (id * 10) + (ord(how[i]) - ord('0')) i += 1 - val.add(checkNil(idgetter)) + val.add(idgetter) lastNum = id + 1 elif how[i + 1] in StartIdent: i += 1 @@ -49,7 +37,7 @@ template formatStr*(howExpr, namegetter, idgetter: expr): expr = while i < how.len and how[i] in Ident: name.add(how[i]) i += 1 - val.add(checkNil(namegetter)) + val.add(namegetter) elif how[i + 1] == '{': i += 2 var name {.inject.} = "" @@ -57,7 +45,7 @@ template formatStr*(howExpr, namegetter, idgetter: expr): expr = name.add(how[i]) i += 1 i += 1 - val.add(checkNil(namegetter)) + val.add(namegetter) else: - raise newException(Exception, "Syntax error in format string at " & $i) + raise newException(ValueError, "Syntax error in format string at " & $i) val diff --git a/lib/impure/osinfo_posix.nim b/lib/impure/osinfo_posix.nim deleted file mode 100644 index 0362fca12..000000000 --- a/lib/impure/osinfo_posix.nim +++ /dev/null @@ -1,10 +0,0 @@ -# -# -# Nim's Runtime Library -# (c) Copyright 2015 Dominik Picheta -# -# See the file "copying.txt", included in this -# distribution, for details about the copyright. -# - -{.error: "This module has been moved to the 'osinfo' nimble package.".} diff --git a/lib/impure/osinfo_win.nim b/lib/impure/osinfo_win.nim deleted file mode 100644 index 0362fca12..000000000 --- a/lib/impure/osinfo_win.nim +++ /dev/null @@ -1,10 +0,0 @@ -# -# -# Nim's Runtime Library -# (c) Copyright 2015 Dominik Picheta -# -# See the file "copying.txt", included in this -# distribution, for details about the copyright. -# - -{.error: "This module has been moved to the 'osinfo' nimble package.".} diff --git a/lib/impure/rdstdin.nim b/lib/impure/rdstdin.nim index b373859f4..f4fc26380 100644 --- a/lib/impure/rdstdin.nim +++ b/lib/impure/rdstdin.nim @@ -9,134 +9,66 @@ ## This module contains code for reading from `stdin`:idx:. On UNIX the ## linenoise library is wrapped and set up to provide default key bindings -## (e.g. you can navigate with the arrow keys). On Windows ``system.readLine`` +## (e.g. you can navigate with the arrow keys). On Windows `system.readLine` ## is used. This suffices because Windows' console already provides the ## wanted functionality. -{.deadCodeElim: on.} +runnableExamples("-r:off"): + echo readLineFromStdin("Is Nim awesome? (Y/n): ") + var line: string + while true: + let ok = readLineFromStdin("How are you? ", line) + if not ok: break # ctrl-C or ctrl-D will cause a break + if line.len > 0: echo line + echo "exiting" -when defined(Windows): - proc readLineFromStdin*(prompt: string): TaintedString {. + +when defined(windows): + when defined(nimPreviewSlimSystem): + import std/syncio + + proc readLineFromStdin*(prompt: string): string {. tags: [ReadIOEffect, WriteIOEffect].} = ## Reads a line from stdin. stdout.write(prompt) + stdout.flushFile() result = readLine(stdin) - proc readLineFromStdin*(prompt: string, line: var TaintedString): bool {. + proc readLineFromStdin*(prompt: string, line: var string): bool {. tags: [ReadIOEffect, WriteIOEffect].} = ## Reads a `line` from stdin. `line` must not be - ## ``nil``! May throw an IO exception. - ## A line of text may be delimited by ``CR``, ``LF`` or - ## ``CRLF``. The newline character(s) are not part of the returned string. - ## Returns ``false`` if the end of the file has been reached, ``true`` - ## otherwise. If ``false`` is returned `line` contains no new data. + ## `nil`! May throw an IO exception. + ## A line of text may be delimited by `CR`, `LF` or + ## `CRLF`. The newline character(s) are not part of the returned string. + ## Returns `false` if the end of the file has been reached, `true` + ## otherwise. If `false` is returned `line` contains no new data. stdout.write(prompt) result = readLine(stdin, line) - import winlean - - const - VK_SHIFT* = 16 - VK_CONTROL* = 17 - VK_MENU* = 18 - KEY_EVENT* = 1 - - type - KEY_EVENT_RECORD = object - bKeyDown: WinBool - wRepeatCount: uint16 - wVirtualKeyCode: uint16 - wVirtualScanCode: uint16 - unicodeChar: uint16 - dwControlKeyState: uint32 - INPUT_RECORD = object - eventType*: int16 - reserved*: int16 - event*: KEY_EVENT_RECORD - safetyBuffer: array[0..5, DWORD] - - proc readConsoleInputW*(hConsoleInput: THANDLE, lpBuffer: var INPUTRECORD, - nLength: uint32, - lpNumberOfEventsRead: var uint32): WINBOOL{. - stdcall, dynlib: "kernel32", importc: "ReadConsoleInputW".} - - proc getch(): uint16 = - let hStdin = getStdHandle(STD_INPUT_HANDLE) - var - irInputRecord: INPUT_RECORD - dwEventsRead: uint32 - - while readConsoleInputW(hStdin, irInputRecord, 1, dwEventsRead) != 0: - if irInputRecord.eventType == KEY_EVENT and - irInputRecord.event.wVirtualKeyCode notin {VK_SHIFT, VK_MENU, VK_CONTROL}: - result = irInputRecord.event.unicodeChar - discard readConsoleInputW(hStdin, irInputRecord, 1, dwEventsRead) - return result - - from unicode import toUTF8, Rune, runeLenAt +elif defined(genode): + proc readLineFromStdin*(prompt: string): string {. + tags: [ReadIOEffect, WriteIOEffect].} = + stdin.readLine() - proc readPasswordFromStdin*(prompt: string, password: var TaintedString): - bool {.tags: [ReadIOEffect, WriteIOEffect].} = - ## Reads a `password` from stdin without printing it. `password` must not - ## be ``nil``! Returns ``false`` if the end of the file has been reached, - ## ``true`` otherwise. - password.setLen(0) - stdout.write(prompt) - while true: - let c = getch() - case c.char - of '\r', chr(0xA): - break - of '\b': - # ensure we delete the whole UTF-8 character: - var i = 0 - var x = 1 - while i < password.len: - x = runeLenAt(password, i) - inc i, x - password.setLen(max(password.len - x, 0)) - else: - password.add(toUTF8(c.Rune)) - stdout.write "\n" + proc readLineFromStdin*(prompt: string, line: var string): bool {. + tags: [ReadIOEffect, WriteIOEffect].} = + stdin.readLine(line) else: - import linenoise, termios, unsigned - - proc readLineFromStdin*(prompt: string): TaintedString {. - tags: [ReadIOEffect, WriteIOEffect].} = - var buffer = linenoise.readLine(prompt) - if isNil(buffer): quit(0) - result = TaintedString($buffer) - if result.string.len > 0: - historyAdd(buffer) - linenoise.free(buffer) + import std/linenoise - proc readLineFromStdin*(prompt: string, line: var TaintedString): bool {. + proc readLineFromStdin*(prompt: string, line: var string): bool {. tags: [ReadIOEffect, WriteIOEffect].} = var buffer = linenoise.readLine(prompt) - if isNil(buffer): quit(0) - line = TaintedString($buffer) - if line.string.len > 0: + if isNil(buffer): + line.setLen(0) + return false + line = $buffer + if line.len > 0: historyAdd(buffer) linenoise.free(buffer) - # XXX how to determine CTRL+D? result = true - proc readPasswordFromStdin*(prompt: string, password: var TaintedString): - bool {.tags: [ReadIOEffect, WriteIOEffect].} = - password.setLen(0) - let fd = stdin.getFileHandle() - var cur, old: Termios - discard fd.tcgetattr(cur.addr) - old = cur - cur.c_lflag = cur.c_lflag and not Cflag(ECHO) - discard fd.tcsetattr(TCSADRAIN, cur.addr) - stdout.write prompt - result = stdin.readLine(password) - stdout.write "\n" - discard fd.tcsetattr(TCSADRAIN, old.addr) - -proc readPasswordFromStdin*(prompt: string): TaintedString = - ## Reads a password from stdin without printing it. - result = TaintedString("") - discard readPasswordFromStdin(prompt, result) + proc readLineFromStdin*(prompt: string): string {.inline.} = + if not readLineFromStdin(prompt, result): + raise newException(IOError, "Linenoise returned nil") diff --git a/lib/impure/re.nim b/lib/impure/re.nim index 60bb6c77f..053c6ab55 100644 --- a/lib/impure/re.nim +++ b/lib/impure/re.nim @@ -7,42 +7,53 @@ # distribution, for details about the copyright. # -## Regular expression support for Nim. Deprecated. Consider using the ``nre`` -## or ``pegs`` modules instead. -## -## **Note:** The 're' proc defaults to the **extended regular expression -## syntax** which lets you use whitespace freely to make your regexes readable. -## However, this means to match whitespace ``\s`` or something similar has -## to be used. +when defined(js): + {.error: "This library needs to be compiled with a c-like backend, and depends on PCRE; See jsre for JS backend.".} + +## Regular expression support for Nim. ## ## This module is implemented by providing a wrapper around the -## `PRCE (Perl-Compatible Regular Expressions) <http://www.pcre.org>`_ -## C library. This means that your application will depend on the PRCE +## `PCRE (Perl-Compatible Regular Expressions) <http://www.pcre.org>`_ +## C library. This means that your application will depend on the PCRE ## library's licence when using this module, which should not be a problem ## though. -## PRCE's licence follows: ## -## .. include:: ../doc/regexprs.txt +## .. note:: There are also alternative nimble packages such as [tinyre](https://github.com/khchen/tinyre) +## and [regex](https://github.com/nitely/nim-regex). +## +## PCRE's licence follows: ## +## .. include:: ../../doc/regexprs.txt +## + +runnableExamples: + ## Unless specified otherwise, `start` parameter in each proc indicates + ## where the scan starts, but outputs are relative to the start of the input + ## string, not to `start`: + doAssert find("uxabc", re"(?<=x|y)ab", start = 1) == 2 # lookbehind assertion + doAssert find("uxabc", re"ab", start = 3) == -1 # we're past `start` => not found + doAssert not match("xabc", re"^abc$", start = 1) + # can't match start of string since we're starting at 1 import - pcre, strutils, rtarrays + std/[pcre, strutils, rtarrays] -{.deprecated.} +when defined(nimPreviewSlimSystem): + import std/syncio const MaxSubpatterns* = 20 ## defines the maximum number of subpatterns that can be captured. - ## This limit still exists for ``replacef`` and ``parallelReplace``. + ## This limit still exists for `replacef` and `parallelReplace`. type RegexFlag* = enum ## options for regular expressions - reIgnoreCase = 0, ## do caseless matching - reMultiLine = 1, ## ``^`` and ``$`` match newlines within data - reDotAll = 2, ## ``.`` matches anything including NL - reExtended = 3, ## ignore whitespace and ``#`` comments - reStudy = 4 ## study the expression (may be omitted if the - ## expression will be used only once) + reIgnoreCase = 0, ## do caseless matching + reMultiLine = 1, ## `^` and `$` match newlines within data + reDotAll = 2, ## `.` matches anything including NL + reExtended = 3, ## ignore whitespace and `#` comments + reStudy = 4 ## study the expression (may be omitted if the + ## expression will be used only once) RegexDesc = object h: ptr Pcre @@ -53,8 +64,17 @@ type RegexError* = object of ValueError ## is raised if the pattern is no valid regular expression. -{.deprecated: [TRegexFlag: RegexFlag, TRegexDesc: RegexDesc, TRegex: Regex, - EInvalidRegEx: RegexError].} +when defined(gcDestructors): + when defined(nimAllowNonVarDestructor): + proc `=destroy`(x: RegexDesc) = + pcre.free_substring(cast[cstring](x.h)) + if not isNil(x.e): + pcre.free_study(x.e) + else: + proc `=destroy`(x: var RegexDesc) = + pcre.free_substring(cast[cstring](x.h)) + if not isNil(x.e): + pcre.free_study(x.e) proc raiseInvalidRegex(msg: string) {.noinline, noreturn.} = var e: ref RegexError @@ -64,76 +84,134 @@ proc raiseInvalidRegex(msg: string) {.noinline, noreturn.} = proc rawCompile(pattern: string, flags: cint): ptr Pcre = var - msg: cstring - offset: cint + msg: cstring = "" + offset: cint = 0 result = pcre.compile(pattern, flags, addr(msg), addr(offset), nil) if result == nil: raiseInvalidRegex($msg & "\n" & pattern & "\n" & spaces(offset) & "^\n") proc finalizeRegEx(x: Regex) = # XXX This is a hack, but PCRE does not export its "free" function properly. - # Sigh. The hack relies on PCRE's implementation (see ``pcre_get.c``). + # Sigh. The hack relies on PCRE's implementation (see `pcre_get.c`). # Fortunately the implementation is unlikely to change. pcre.free_substring(cast[cstring](x.h)) if not isNil(x.e): - pcre.free_substring(cast[cstring](x.e)) + pcre.free_study(x.e) -proc re*(s: string, flags = {reExtended, reStudy}): Regex {.deprecated.} = - ## Constructor of regular expressions. Note that Nim's - ## extended raw string literals support this syntax ``re"[abc]"`` as - ## a short form for ``re(r"[abc]")``. - new(result, finalizeRegEx) +proc re*(s: string, flags = {reStudy}): Regex = + ## Constructor of regular expressions. + ## + ## Note that Nim's + ## extended raw string literals support the syntax `re"[abc]"` as + ## a short form for `re(r"[abc]")`. Also note that since this + ## compiles the regular expression, which is expensive, you should + ## avoid putting it directly in the arguments of the functions like + ## the examples show below if you plan to use it a lot of times, as + ## this will hurt performance immensely. (e.g. outside a loop, ...) + when defined(gcDestructors): + result = Regex() + else: + new(result, finalizeRegEx) result.h = rawCompile(s, cast[cint](flags - {reStudy})) if reStudy in flags: - var msg: cstring - result.e = pcre.study(result.h, 0, addr msg) + var msg: cstring = "" + var options: cint = 0 + var hasJit: cint = 0 + if pcre.config(pcre.CONFIG_JIT, addr hasJit) == 0: + if hasJit == 1'i32: + options = pcre.STUDY_JIT_COMPILE + result.e = pcre.study(result.h, options, addr msg) if not isNil(msg): raiseInvalidRegex($msg) -proc matchOrFind(s: string, pattern: Regex, matches: var openArray[string], - start, flags: cint): cint = +proc rex*(s: string, flags = {reStudy, reExtended}): Regex = + ## Constructor for extended regular expressions. + ## + ## The extended means that comments starting with `#` and + ## whitespace are ignored. + result = re(s, flags) + +proc bufSubstr(b: cstring, sPos, ePos: int): string {.inline.} = + ## Return a Nim string built from a slice of a cstring buffer. + ## Don't assume cstring is '\0' terminated + let sz = ePos - sPos + result = newString(sz+1) + copyMem(addr(result[0]), unsafeAddr(b[sPos]), sz) + result.setLen(sz) + +proc matchOrFind(buf: cstring, pattern: Regex, matches: var openArray[string], + start, bufSize, flags: cint): cint = var rtarray = initRtArray[cint]((matches.len+1)*3) rawMatches = rtarray.getRawData - res = pcre.exec(pattern.h, pattern.e, s, len(s).cint, start, flags, + res = pcre.exec(pattern.h, pattern.e, buf, bufSize, start, flags, cast[ptr cint](rawMatches), (matches.len+1).cint*3) if res < 0'i32: return res for i in 1..int(res)-1: var a = rawMatches[i * 2] var b = rawMatches[i * 2 + 1] - if a >= 0'i32: matches[i-1] = substr(s, int(a), int(b)-1) - else: matches[i-1] = nil + if a >= 0'i32: + matches[i-1] = bufSubstr(buf, int(a), int(b)) + else: matches[i-1] = "" return rawMatches[1] - rawMatches[0] -proc findBounds*(s: string, pattern: Regex, matches: var openArray[string], - start = 0): tuple[first, last: int] = - ## returns the starting position and end position of `pattern` in `s` +const MaxReBufSize* = high(cint) + ## Maximum PCRE (API 1) buffer start/size equal to `high(cint)`, which even + ## for 64-bit systems can be either 2`31`:sup:-1 or 2`63`:sup:-1. + +proc findBounds*(buf: cstring, pattern: Regex, matches: var openArray[string], + start = 0, bufSize: int): tuple[first, last: int] = + ## returns the starting position and end position of `pattern` in `buf` + ## (where `buf` has length `bufSize` and is not necessarily `'\0'` terminated), ## and the captured ## substrings in the array `matches`. If it does not match, nothing - ## is written into `matches` and ``(-1,0)`` is returned. + ## is written into `matches` and `(-1,0)` is returned. + ## + ## Note: The memory for `matches` needs to be allocated before this function is + ## called, otherwise it will just remain empty. var rtarray = initRtArray[cint]((matches.len+1)*3) rawMatches = rtarray.getRawData - res = pcre.exec(pattern.h, pattern.e, s, len(s).cint, start.cint, 0'i32, + res = pcre.exec(pattern.h, pattern.e, buf, bufSize.cint, start.cint, 0'i32, cast[ptr cint](rawMatches), (matches.len+1).cint*3) if res < 0'i32: return (-1, 0) for i in 1..int(res)-1: var a = rawMatches[i * 2] var b = rawMatches[i * 2 + 1] - if a >= 0'i32: matches[i-1] = substr(s, int(a), int(b)-1) - else: matches[i-1] = nil + if a >= 0'i32: matches[i-1] = bufSubstr(buf, int(a), int(b)) + else: matches[i-1] = "" return (rawMatches[0].int, rawMatches[1].int - 1) -proc findBounds*(s: string, pattern: Regex, +proc findBounds*(s: string, pattern: Regex, matches: var openArray[string], + start = 0): tuple[first, last: int] {.inline.} = + ## returns the starting position and end position of `pattern` in `s` + ## and the captured substrings in the array `matches`. + ## If it does not match, nothing + ## is written into `matches` and `(-1,0)` is returned. + ## + ## .. note:: The memory for `matches` needs to be allocated before this function is called, otherwise it will just remain empty. + runnableExamples: + var matches = newSeq[string](1) + let (first, last) = findBounds("Hello World", re"(W\w+)", matches) + doAssert first == 6 + doAssert last == 10 + doAssert matches[0] == "World" + result = findBounds(cstring(s), pattern, matches, + min(start, MaxReBufSize), min(s.len, MaxReBufSize)) + +proc findBounds*(buf: cstring, pattern: Regex, matches: var openArray[tuple[first, last: int]], - start = 0): tuple[first, last: int] = - ## returns the starting position and end position of ``pattern`` in ``s`` + start = 0, bufSize: int): tuple[first, last: int] = + ## returns the starting position and end position of `pattern` in `buf` + ## (where `buf` has length `bufSize` and is not necessarily `'\0'` terminated), ## and the captured substrings in the array `matches`. ## If it does not match, nothing is written into `matches` and - ## ``(-1,0)`` is returned. + ## `(-1,0)` is returned. + ## + ## .. note:: The memory for `matches` needs to be allocated before this function is called, otherwise it will just remain empty. var rtarray = initRtArray[cint]((matches.len+1)*3) rawMatches = rtarray.getRawData - res = pcre.exec(pattern.h, pattern.e, s, len(s).cint, start.cint, 0'i32, + res = pcre.exec(pattern.h, pattern.e, buf, bufSize.cint, start.cint, 0'i32, cast[ptr cint](rawMatches), (matches.len+1).cint*3) if res < 0'i32: return (-1, 0) for i in 1..int(res)-1: @@ -144,80 +222,188 @@ proc findBounds*(s: string, pattern: Regex, return (rawMatches[0].int, rawMatches[1].int - 1) proc findBounds*(s: string, pattern: Regex, - start = 0): tuple[first, last: int] = - ## returns the starting position and end position of ``pattern`` in ``s``. - ## If it does not match, ``(-1,0)`` is returned. + matches: var openArray[tuple[first, last: int]], + start = 0): tuple[first, last: int] {.inline.} = + ## returns the starting position and end position of `pattern` in `s` + ## and the captured substrings in the array `matches`. + ## If it does not match, nothing is written into `matches` and + ## `(-1,0)` is returned. + ## + ## .. note:: The memory for `matches` needs to be allocated before this function is called, otherwise it will just remain empty. + runnableExamples: + var matches = newSeq[tuple[first, last: int]](1) + let (first, last) = findBounds("Hello World", re"(\w+)", matches) + doAssert first == 0 + doAssert last == 4 + doAssert matches[0] == (0, 4) + result = findBounds(cstring(s), pattern, matches, + min(start, MaxReBufSize), min(s.len, MaxReBufSize)) + +proc findBoundsImpl(buf: cstring, pattern: Regex, + start = 0, bufSize = 0, flags = 0): tuple[first, last: int] = + var rtarray = initRtArray[cint](3) + let rawMatches = rtarray.getRawData + let res = pcre.exec(pattern.h, pattern.e, buf, bufSize.cint, start.cint, flags.int32, + cast[ptr cint](rawMatches), 3) + + if res < 0'i32: + result = (-1, 0) + else: + result = (int(rawMatches[0]), int(rawMatches[1]-1)) + +proc findBounds*(buf: cstring, pattern: Regex, + start = 0, bufSize: int): tuple[first, last: int] = + ## returns the `first` and `last` position of `pattern` in `buf`, + ## where `buf` has length `bufSize` (not necessarily `'\0'` terminated). + ## If it does not match, `(-1,0)` is returned. var rtarray = initRtArray[cint](3) rawMatches = rtarray.getRawData - res = pcre.exec(pattern.h, nil, s, len(s).cint, start.cint, 0'i32, + res = pcre.exec(pattern.h, pattern.e, buf, bufSize.cint, start.cint, 0'i32, cast[ptr cint](rawMatches), 3) if res < 0'i32: return (int(res), 0) return (int(rawMatches[0]), int(rawMatches[1]-1)) -proc matchOrFind(s: string, pattern: Regex, start, flags: cint): cint = +proc findBounds*(s: string, pattern: Regex, + start = 0): tuple[first, last: int] {.inline.} = + ## returns the `first` and `last` position of `pattern` in `s`. + ## If it does not match, `(-1,0)` is returned. + ## + ## Note: there is a speed improvement if the matches do not need to be captured. + runnableExamples: + assert findBounds("01234abc89", re"abc") == (5,7) + result = findBounds(cstring(s), pattern, + min(start, MaxReBufSize), min(s.len, MaxReBufSize)) + +proc matchOrFind(buf: cstring, pattern: Regex, start, bufSize: int, flags: cint): cint = var rtarray = initRtArray[cint](3) rawMatches = rtarray.getRawData - result = pcre.exec(pattern.h, pattern.e, s, len(s).cint, start, flags, + result = pcre.exec(pattern.h, pattern.e, buf, bufSize.cint, start.cint, flags, cast[ptr cint](rawMatches), 3) if result >= 0'i32: result = rawMatches[1] - rawMatches[0] proc matchLen*(s: string, pattern: Regex, matches: var openArray[string], - start = 0): int = - ## the same as ``match``, but it returns the length of the match, - ## if there is no match, -1 is returned. Note that a match length + start = 0): int {.inline.} = + ## the same as `match`, but it returns the length of the match, + ## if there is no match, `-1` is returned. Note that a match length ## of zero can happen. - return matchOrFind(s, pattern, matches, start.cint, pcre.ANCHORED) + ## + ## .. note:: The memory for `matches` needs to be allocated before this function is called, otherwise it will just remain empty. + result = matchOrFind(cstring(s), pattern, matches, start.cint, s.len.cint, pcre.ANCHORED) -proc matchLen*(s: string, pattern: Regex, start = 0): int = - ## the same as ``match``, but it returns the length of the match, - ## if there is no match, -1 is returned. Note that a match length +proc matchLen*(buf: cstring, pattern: Regex, matches: var openArray[string], + start = 0, bufSize: int): int {.inline.} = + ## the same as `match`, but it returns the length of the match, + ## if there is no match, `-1` is returned. Note that a match length + ## of zero can happen. + ## + ## .. note:: The memory for `matches` needs to be allocated before this function is called, otherwise it will just remain empty. + return matchOrFind(buf, pattern, matches, start.cint, bufSize.cint, pcre.ANCHORED) + +proc matchLen*(s: string, pattern: Regex, start = 0): int {.inline.} = + ## the same as `match`, but it returns the length of the match, + ## if there is no match, `-1` is returned. Note that a match length + ## of zero can happen. + ## + runnableExamples: + doAssert matchLen("abcdefg", re"cde", 2) == 3 + doAssert matchLen("abcdefg", re"abcde") == 5 + doAssert matchLen("abcdefg", re"cde") == -1 + result = matchOrFind(cstring(s), pattern, start.cint, s.len.cint, pcre.ANCHORED) + +proc matchLen*(buf: cstring, pattern: Regex, start = 0, bufSize: int): int {.inline.} = + ## the same as `match`, but it returns the length of the match, + ## if there is no match, `-1` is returned. Note that a match length ## of zero can happen. - return matchOrFind(s, pattern, start.cint, pcre.ANCHORED) + result = matchOrFind(buf, pattern, start.cint, bufSize, pcre.ANCHORED) -proc match*(s: string, pattern: Regex, start = 0): bool = - ## returns ``true`` if ``s[start..]`` matches the ``pattern``. - result = matchLen(s, pattern, start) != -1 +proc match*(s: string, pattern: Regex, start = 0): bool {.inline.} = + ## returns `true` if `s[start..]` matches the `pattern`. + result = matchLen(cstring(s), pattern, start, s.len) != -1 proc match*(s: string, pattern: Regex, matches: var openArray[string], - start = 0): bool = - ## returns ``true`` if ``s[start..]`` matches the ``pattern`` and - ## the captured substrings in the array ``matches``. If it does not - ## match, nothing is written into ``matches`` and ``false`` is + start = 0): bool {.inline.} = + ## returns `true` if `s[start..]` matches the `pattern` and + ## the captured substrings in the array `matches`. If it does not + ## match, nothing is written into `matches` and `false` is + ## returned. + ## + ## .. note:: The memory for `matches` needs to be allocated before this function is called, otherwise it will just remain empty. + runnableExamples: + import std/sequtils + var matches: array[2, string] + if match("abcdefg", re"c(d)ef(g)", matches, 2): + doAssert toSeq(matches) == @["d", "g"] + result = matchLen(cstring(s), pattern, matches, start, s.len) != -1 + +proc match*(buf: cstring, pattern: Regex, matches: var openArray[string], + start = 0, bufSize: int): bool {.inline.} = + ## returns `true` if `buf[start..<bufSize]` matches the `pattern` and + ## the captured substrings in the array `matches`. If it does not + ## match, nothing is written into `matches` and `false` is ## returned. - result = matchLen(s, pattern, matches, start) != -1 + ## `buf` has length `bufSize` (not necessarily `'\0'` terminated). + ## + ## .. note:: The memory for `matches` needs to be allocated before this function is called, otherwise it will just remain empty. + result = matchLen(buf, pattern, matches, start, bufSize) != -1 -proc find*(s: string, pattern: Regex, matches: var openArray[string], - start = 0): int = - ## returns the starting position of ``pattern`` in ``s`` and the captured - ## substrings in the array ``matches``. If it does not match, nothing - ## is written into ``matches`` and -1 is returned. +proc find*(buf: cstring, pattern: Regex, matches: var openArray[string], + start = 0, bufSize: int): int = + ## returns the starting position of `pattern` in `buf` and the captured + ## substrings in the array `matches`. If it does not match, nothing + ## is written into `matches` and `-1` is returned. + ## `buf` has length `bufSize` (not necessarily `'\0'` terminated). + ## + ## .. note:: The memory for `matches` needs to be allocated before this function is called, otherwise it will just remain empty. var rtarray = initRtArray[cint]((matches.len+1)*3) rawMatches = rtarray.getRawData - res = pcre.exec(pattern.h, pattern.e, s, len(s).cint, start.cint, 0'i32, + res = pcre.exec(pattern.h, pattern.e, buf, bufSize.cint, start.cint, 0'i32, cast[ptr cint](rawMatches), (matches.len+1).cint*3) if res < 0'i32: return res for i in 1..int(res)-1: var a = rawMatches[i * 2] var b = rawMatches[i * 2 + 1] - if a >= 0'i32: matches[i-1] = substr(s, int(a), int(b)-1) - else: matches[i-1] = nil + if a >= 0'i32: matches[i-1] = bufSubstr(buf, int(a), int(b)) + else: matches[i-1] = "" return rawMatches[0] -proc find*(s: string, pattern: Regex, start = 0): int = - ## returns the starting position of ``pattern`` in ``s``. If it does not - ## match, -1 is returned. +proc find*(s: string, pattern: Regex, matches: var openArray[string], + start = 0): int {.inline.} = + ## returns the starting position of `pattern` in `s` and the captured + ## substrings in the array `matches`. If it does not match, nothing + ## is written into `matches` and `-1` is returned. + ## + ## .. note:: The memory for `matches` needs to be allocated before this function is called, otherwise it will just remain empty. + result = find(cstring(s), pattern, matches, start, s.len) + +proc find*(buf: cstring, pattern: Regex, start = 0, bufSize: int): int = + ## returns the starting position of `pattern` in `buf`, + ## where `buf` has length `bufSize` (not necessarily `'\0'` terminated). + ## If it does not match, `-1` is returned. var rtarray = initRtArray[cint](3) rawMatches = rtarray.getRawData - res = pcre.exec(pattern.h, nil, s, len(s).cint, start.cint, 0'i32, + res = pcre.exec(pattern.h, pattern.e, buf, bufSize.cint, start.cint, 0'i32, cast[ptr cint](rawMatches), 3) if res < 0'i32: return res return rawMatches[0] +proc find*(s: string, pattern: Regex, start = 0): int {.inline.} = + ## returns the starting position of `pattern` in `s`. If it does not + ## match, `-1` is returned. We start the scan at `start`. + runnableExamples: + doAssert find("abcdefg", re"cde") == 2 + doAssert find("abcdefg", re"abc") == 0 + doAssert find("abcdefg", re"zz") == -1 # not found + doAssert find("abcdefg", re"cde", start = 2) == 2 # still 2 + doAssert find("abcdefg", re"cde", start = 3) == -1 # we're past the start position + doAssert find("xabc", re"(?<=x|y)abc", start = 1) == 1 + # lookbehind assertion `(?<=x|y)` can look behind `start` + result = find(cstring(s), pattern, start, s.len) + iterator findAll*(s: string, pattern: Regex, start = 0): string = ## Yields all matching *substrings* of `s` that match `pattern`. ## @@ -237,32 +423,47 @@ iterator findAll*(s: string, pattern: Regex, start = 0): string = yield substr(s, int(a), int(b)-1) i = b -proc findAll*(s: string, pattern: Regex, start = 0): seq[string] = - ## returns all matching *substrings* of `s` that match `pattern`. - ## If it does not match, @[] is returned. - accumulateResult(findAll(s, pattern, start)) - -when not defined(nimhygiene): - {.pragma: inject.} - -template `=~` *(s: string, pattern: Regex): expr = - ## This calls ``match`` with an implicit declared ``matches`` array that - ## can be used in the scope of the ``=~`` call: - ## - ## .. code-block:: nim - ## - ## if line =~ re"\s*(\w+)\s*\=\s*(\w+)": - ## # matches a key=value pair: - ## echo("Key: ", matches[0]) - ## echo("Value: ", matches[1]) - ## elif line =~ re"\s*(\#.*)": - ## # matches a comment - ## # note that the implicit ``matches`` array is different from the - ## # ``matches`` array of the first branch - ## echo("comment: ", matches[0]) - ## else: - ## echo("syntax error") +iterator findAll*(buf: cstring, pattern: Regex, start = 0, bufSize: int): string = + ## Yields all matching `substrings` of `s` that match `pattern`. ## + ## Note that since this is an iterator you should not modify the string you + ## are iterating over: bad things could happen. + var + i = int32(start) + rtarray = initRtArray[cint](3) + rawMatches = rtarray.getRawData + while true: + let res = pcre.exec(pattern.h, pattern.e, buf, bufSize.cint, i, 0'i32, + cast[ptr cint](rawMatches), 3) + if res < 0'i32: break + let a = rawMatches[0] + let b = rawMatches[1] + if a == b and a == i: break + var str = newString(b-a) + copyMem(str[0].addr, unsafeAddr(buf[a]), b-a) + yield str + i = b + +proc findAll*(s: string, pattern: Regex, start = 0): seq[string] {.inline.} = + ## returns all matching `substrings` of `s` that match `pattern`. + ## If it does not match, `@[]` is returned. + result = @[] + for x in findAll(s, pattern, start): result.add x + +template `=~` *(s: string, pattern: Regex): untyped = + ## This calls `match` with an implicit declared `matches` array that + ## can be used in the scope of the `=~` call: + runnableExamples: + proc parse(line: string): string = + if line =~ re"\s*(\w+)\s*\=\s*(\w+)": # matches a key=value pair: + result = $(matches[0], matches[1]) + elif line =~ re"\s*(\#.*)": # matches a comment + # note that the implicit `matches` array is different from 1st branch + result = $(matches[0],) + else: raiseAssert "unreachable" + doAssert not declared(matches) + doAssert parse("NAME = LENA") == """("NAME", "LENA")""" + doAssert parse(" # comment ... ") == """("# comment ... ",)""" bind MaxSubpatterns when not declaredInScope(matches): var matches {.inject.}: array[MaxSubpatterns, string] @@ -270,73 +471,67 @@ template `=~` *(s: string, pattern: Regex): expr = # ------------------------- more string handling ------------------------------ -proc contains*(s: string, pattern: Regex, start = 0): bool = - ## same as ``find(s, pattern, start) >= 0`` +proc contains*(s: string, pattern: Regex, start = 0): bool {.inline.} = + ## same as `find(s, pattern, start) >= 0` return find(s, pattern, start) >= 0 proc contains*(s: string, pattern: Regex, matches: var openArray[string], - start = 0): bool = - ## same as ``find(s, pattern, matches, start) >= 0`` + start = 0): bool {.inline.} = + ## same as `find(s, pattern, matches, start) >= 0` + ## + ## .. note:: The memory for `matches` needs to be allocated before this function is called, otherwise it will just remain empty. return find(s, pattern, matches, start) >= 0 -proc startsWith*(s: string, prefix: Regex): bool = +proc startsWith*(s: string, prefix: Regex): bool {.inline.} = ## returns true if `s` starts with the pattern `prefix` result = matchLen(s, prefix) >= 0 -proc endsWith*(s: string, suffix: Regex): bool = - ## returns true if `s` ends with the pattern `prefix` +proc endsWith*(s: string, suffix: Regex): bool {.inline.} = + ## returns true if `s` ends with the pattern `suffix` for i in 0 .. s.len-1: if matchLen(s, suffix, i) == s.len - i: return true proc replace*(s: string, sub: Regex, by = ""): string = ## Replaces `sub` in `s` by the string `by`. Captures cannot be - ## accessed in `by`. Examples: - ## - ## .. code-block:: nim - ## "var1=key; var2=key2".replace(re"(\w+)=(\w+)") - ## - ## Results in: - ## - ## .. code-block:: nim - ## - ## "; " + ## accessed in `by`. + runnableExamples: + doAssert "var1=key; var2=key2".replace(re"(\w+)=(\w+)") == "; " + doAssert "var1=key; var2=key2".replace(re"(\w+)=(\w+)", "?") == "?; ?" result = "" var prev = 0 - while true: - var match = findBounds(s, sub, prev) + var flags = int32(0) + while prev < s.len: + var match = findBoundsImpl(s.cstring, sub, prev, s.len, flags) + flags = 0 if match.first < 0: break add(result, substr(s, prev, match.first-1)) add(result, by) + if match.first > match.last: + # 0-len match + flags = pcre.NOTEMPTY_ATSTART prev = match.last + 1 add(result, substr(s, prev)) proc replacef*(s: string, sub: Regex, by: string): string = ## Replaces `sub` in `s` by the string `by`. Captures can be accessed in `by` - ## with the notation ``$i`` and ``$#`` (see strutils.`%`). Examples: - ## - ## .. code-block:: nim - ## "var1=key; var2=key2".replacef(re"(\w+)=(\w+)", "$1<-$2$2") - ## - ## Results in: - ## - ## .. code-block:: nim - ## - ## "var1<-keykey; val2<-key2key2" + ## with the notation `$i` and `$#` (see strutils.\`%\`). + runnableExamples: + doAssert "var1=key; var2=key2".replacef(re"(\w+)=(\w+)", "$1<-$2$2") == + "var1<-keykey; var2<-key2key2" result = "" var caps: array[MaxSubpatterns, string] var prev = 0 - while true: + while prev < s.len: var match = findBounds(s, sub, caps, prev) if match.first < 0: break - assert result != nil - assert s != nil add(result, substr(s, prev, match.first-1)) addf(result, by, caps) + if match.last + 1 == prev: break prev = match.last + 1 add(result, substr(s, prev)) -proc parallelReplace*(s: string, subs: openArray[ - tuple[pattern: Regex, repl: string]]): string = +proc multiReplace*(s: string, subs: openArray[ + tuple[pattern: Regex, repl: string]]): string = ## Returns a modified copy of `s` with the substitutions in `subs` ## applied in parallel. result = "" @@ -358,49 +553,50 @@ proc parallelReplace*(s: string, subs: openArray[ proc transformFile*(infile, outfile: string, subs: openArray[tuple[pattern: Regex, repl: string]]) = ## reads in the file `infile`, performs a parallel replacement (calls - ## `parallelReplace`) and writes back to `outfile`. Raises ``EIO`` if an + ## `parallelReplace`) and writes back to `outfile`. Raises `IOError` if an ## error occurs. This is supposed to be used for quick scripting. - var x = readFile(infile).string - writeFile(outfile, x.parallelReplace(subs)) + var x = readFile(infile) + writeFile(outfile, x.multiReplace(subs)) -iterator split*(s: string, sep: Regex): string = +iterator split*(s: string, sep: Regex; maxsplit = -1): string = ## Splits the string `s` into substrings. ## - ## Substrings are separated by the regular expression `sep`. - ## Examples: - ## - ## .. code-block:: nim - ## for word in split("00232this02939is39an22example111", re"\d+"): - ## writeLine(stdout, word) - ## - ## Results in: - ## - ## .. code-block:: nim - ## "" - ## "this" - ## "is" - ## "an" - ## "example" - ## "" - ## - var - first = -1 - last = -1 - while last < len(s): - var x = matchLen(s, sep, last) - if x > 0: inc(last, x) - first = last - if x == 0: inc(last) + ## Substrings are separated by the regular expression `sep` + ## (and the portion matched by `sep` is not returned). + runnableExamples: + import std/sequtils + doAssert toSeq(split("00232this02939is39an22example111", re"\d+")) == + @["", "this", "is", "an", "example", ""] + var last = 0 + var splits = maxsplit + var x = -1 + if len(s) == 0: + last = 1 + if matchLen(s, sep, 0) == 0: + x = 0 + while last <= len(s): + var first = last + var sepLen = 1 + if x == 0: + inc(last) while last < len(s): x = matchLen(s, sep, last) - if x >= 0: break + if x >= 0: + sepLen = x + break inc(last) - if first <= last: - yield substr(s, first, last-1) - -proc split*(s: string, sep: Regex): seq[string] = - ## Splits the string `s` into substrings. - accumulateResult(split(s, sep)) + if splits == 0: last = len(s) + yield substr(s, first, last-1) + if splits == 0: break + dec(splits) + inc(last, sepLen) + +proc split*(s: string, sep: Regex, maxsplit = -1): seq[string] {.inline.} = + ## Splits the string `s` into a seq of substrings. + ## + ## The portion matched by `sep` is not returned. + result = @[] + for x in split(s, sep, maxsplit): result.add x proc escapeRe*(s: string): string = ## escapes `s` so that it is matched verbatim when used as a regular @@ -413,97 +609,3 @@ proc escapeRe*(s: string): string = else: result.add("\\x") result.add(toHex(ord(c), 2)) - -const ## common regular expressions - reIdentifier* {.deprecated.} = r"\b[a-zA-Z_]+[a-zA-Z_0-9]*\b" - ## describes an identifier - reNatural* {.deprecated.} = r"\b\d+\b" - ## describes a natural number - reInteger* {.deprecated.} = r"\b[-+]?\d+\b" - ## describes an integer - reHex* {.deprecated.} = r"\b0[xX][0-9a-fA-F]+\b" - ## describes a hexadecimal number - reBinary* {.deprecated.} = r"\b0[bB][01]+\b" - ## describes a binary number (example: 0b11101) - reOctal* {.deprecated.} = r"\b0[oO][0-7]+\b" - ## describes an octal number (example: 0o777) - reFloat* {.deprecated.} = r"\b[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?\b" - ## describes a floating point number - reEmail* {.deprecated.} = r"\b[a-zA-Z0-9!#$%&'*+/=?^_`{|}~\-]+(?:\. &" & - r"[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*@" & - r"(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+" & - r"(?:[a-zA-Z]{2}|com|org|net|gov|mil|biz|" & - r"info|mobi|name|aero|jobs|museum)\b" - ## describes a common email address - reURL* {.deprecated.} = r"\b(http(s)?|ftp|gopher|telnet|file|notes|ms-help)" & - r":((//)|(\\\\))+[\w\d:#@%/;$()~_?\+\-\=\\\.\&]*\b" - ## describes an URL - -when isMainModule: - assert match("(a b c)", re"\( .* \)") - assert match("WHiLe", re("while", {reIgnoreCase})) - - assert "0158787".match(re"\d+") - assert "ABC 0232".match(re"\w+\s+\d+") - assert "ABC".match(re"\d+ | \w+") - - assert matchLen("key", re(reIdentifier)) == 3 - - var pattern = re"[a-z0-9]+\s*=\s*[a-z0-9]+" - assert matchLen("key1= cal9", pattern) == 11 - - assert find("_____abc_______", re"abc") == 5 - - var matches: array[6, string] - if match("abcdefg", re"c(d)ef(g)", matches, 2): - assert matches[0] == "d" - assert matches[1] == "g" - else: - assert false - - if "abc" =~ re"(a)bcxyz|(\w+)": - assert matches[1] == "abc" - else: - assert false - - if "abc" =~ re"(cba)?.*": - assert matches[0] == nil - else: assert false - - if "abc" =~ re"().*": - assert matches[0] == "" - else: assert false - - assert "var1=key; var2=key2".endsWith(re"\w+=\w+") - assert("var1=key; var2=key2".replacef(re"(\w+)=(\w+)", "$1<-$2$2") == - "var1<-keykey; var2<-key2key2") - assert("var1=key; var2=key2".replace(re"(\w+)=(\w+)", "$1<-$2$2") == - "$1<-$2$2; $1<-$2$2") - - var accum: seq[string] = @[] - for word in split("00232this02939is39an22example111", re"\d+"): - accum.add(word) - assert(accum == @["", "this", "is", "an", "example", ""]) - - accum = @[] - for word in split("AAA : : BBB", re"\s*:\s*"): - accum.add(word) - assert(accum == @["AAA", "", "BBB"]) - - for x in findAll("abcdef", re"^{.}", 3): - assert x == "d" - accum = @[] - for x in findAll("abcdef", re".", 3): - accum.add(x) - assert(accum == @["d", "e", "f"]) - - assert("XYZ".find(re"^\d*") == 0) - assert("XYZ".match(re"^\d*") == true) - - block: - var matches: array[16, string] - if match("abcdefghijklmnop", re"(a)(b)(c)(d)(e)(f)(g)(h)(i)(j)(k)(l)(m)(n)(o)(p)", matches): - for i in 0..matches.high: - assert matches[i] == $chr(i + 'a'.ord) - else: - assert false diff --git a/lib/impure/ssl.nim b/lib/impure/ssl.nim deleted file mode 100644 index 721e5ce51..000000000 --- a/lib/impure/ssl.nim +++ /dev/null @@ -1,97 +0,0 @@ -# -# -# Nim's Runtime Library -# (c) Copyright 2012 Dominik Picheta -# -# See the file "copying.txt", included in this -# distribution, for details about the copyright. -# - -## This module provides an easy to use sockets-style -## nim interface to the OpenSSL library. - -{.deprecated.} - -import openssl, strutils, os - -type - SecureSocket* = object - ssl: SslPtr - bio: BIO -{.deprecated: [TSecureSocket: SecureSocket].} - -proc connect*(sock: var SecureSocket, address: string, - port: int): int = - ## Connects to the specified `address` on the specified `port`. - ## Returns the result of the certificate validation. - SslLoadErrorStrings() - ERR_load_BIO_strings() - - if SSL_library_init() != 1: - raiseOSError(osLastError()) - - var ctx = SSL_CTX_new(SSLv23_client_method()) - if ctx == nil: - ERR_print_errors_fp(stderr) - raiseOSError(osLastError()) - - #if SSL_CTX_load_verify_locations(ctx, - # "/tmp/openssl-0.9.8e/certs/vsign1.pem", NIL) == 0: - # echo("Failed load verify locations") - # ERR_print_errors_fp(stderr) - - sock.bio = BIO_new_ssl_connect(ctx) - if BIO_get_ssl(sock.bio, addr(sock.ssl)) == 0: - raiseOSError(osLastError()) - - if BIO_set_conn_hostname(sock.bio, address & ":" & $port) != 1: - raiseOSError(osLastError()) - - if BIO_do_connect(sock.bio) <= 0: - ERR_print_errors_fp(stderr) - raiseOSError(osLastError()) - - result = SSL_get_verify_result(sock.ssl) - -proc recvLine*(sock: SecureSocket, line: var TaintedString): bool = - ## Acts in a similar fashion to the `recvLine` in the sockets module. - ## Returns false when no data is available to be read. - ## `Line` must be initialized and not nil! - setLen(line.string, 0) - while true: - var c: array[0..0, char] - var n = BIO_read(sock.bio, c, c.len.cint) - if n <= 0: return false - if c[0] == '\r': - n = BIO_read(sock.bio, c, c.len.cint) - if n > 0 and c[0] == '\L': - return true - elif n <= 0: - return false - elif c[0] == '\L': return true - add(line.string, c) - - -proc send*(sock: SecureSocket, data: string) = - ## Writes `data` to the socket. - if BIO_write(sock.bio, data, data.len.cint) <= 0: - raiseOSError(osLastError()) - -proc close*(sock: SecureSocket) = - ## Closes the socket - if BIO_free(sock.bio) <= 0: - ERR_print_errors_fp(stderr) - raiseOSError(osLastError()) - -when not defined(testing) and isMainModule: - var s: SecureSocket - echo connect(s, "smtp.gmail.com", 465) - - #var buffer: array[0..255, char] - #echo BIO_read(bio, buffer, buffer.len) - var buffer: string = "" - - echo s.recvLine(buffer) - echo buffer - echo buffer.len - |