diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 0296247..32da09e 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -32,21 +32,25 @@ jobs:
steps:
- name: Check out RedisX
uses: actions/checkout@v4
-
+
- name: Check out xchange
uses: actions/checkout@v4
with:
repository: Smithsonian/xchange
path: xchange
-
+
+ - name: Install build dependencies
+ run: sudo apt-get install libpopt-dev
+
- name: Build static library
run: make static
-
+
- name: Build xchange dependency
run: make -C xchange shared
-
+
- name: Build shared library
run: make shared
-
-
+ - name: Build tools
+ run: make tools
+
diff --git a/.github/workflows/install.yml b/.github/workflows/install.yml
index e36da29..a870115 100644
--- a/.github/workflows/install.yml
+++ b/.github/workflows/install.yml
@@ -28,8 +28,8 @@ jobs:
env:
CC: gcc
steps:
- - name: install doxygen
- run: sudo apt-get install doxygen
+ - name: install build deps
+ run: sudo apt-get install doxygen libpopt-dev
- name: Check out RedisX
uses: actions/checkout@v4
diff --git a/Makefile b/Makefile
index 2e05788..f75bd91 100644
--- a/Makefile
+++ b/Makefile
@@ -34,7 +34,7 @@ LDFLAGS += -pthread
# Build for distribution
.PHONY: distro
-distro: shared $(DOC_TARGETS)
+distro: shared tools $(DOC_TARGETS)
# Build everything...
.PHONY: all
@@ -48,6 +48,15 @@ shared: $(LIB)/libredisx.so
.PHONY: static
static: $(LIB)/libredisx.a
+# Command-line tools
+.PHONY: tools
+ifdef STATICLINK
+tools: static
+else
+tools: shared
+endif
+ make -f tools.mk
+
# Run regression tests
.PHONY: test
test:
@@ -92,6 +101,11 @@ $(LIB)/libredisx.so.$(SO_VERSION): $(SOURCES)
# Static library
$(LIB)/libredisx.a: $(OBJECTS)
+# redisx-cli
+$(BIN)/redisx-cli: LDFLAGS += -lredisx
+$(BIN)/redisx-cli: $(OBJ)/redisx-cli.o
+
+
README-redisx.md: README.md
LINE=`sed -n '/\# /{=;q;}' $<` && tail -n +$$((LINE+2)) $< > $@
@@ -125,6 +139,7 @@ pdf:
# See https://www.gnu.org/prep/standards/html_node/Directory-Variables.html
prefix ?= /usr
exec_prefix ?= $(prefix)
+bindir ?= $(exec_prefix)/bin
libdir ?= $(exec_prefix)/lib
includedir ?= $(prefix)/include
datarootdir ?= $(prefix)/share
@@ -138,7 +153,7 @@ INSTALL_PROGRAM ?= install
INSTALL_DATA ?= install -m 644
.PHONY: install
-install: install-libs install-headers install-html
+install: install-libs install-bin install-headers install-html
.PHONY: install-libs
install-libs:
@@ -150,6 +165,16 @@ else
@echo "WARNING! Skipping libs install: needs 'shared' and/or 'static'"
endif
+.PHONY: install-bin
+install-bin:
+ifneq ($(wildcard $(BIN)/*),)
+ @echo "installing executables to $(DESTDIR)$(bindir)"
+ install -d $(DESTDIR)$(bindir)
+ $(INSTALL_PROGRAM) -D $(BIN)/* $(DESTDIR)$(bindir)/
+else
+ @echo "WARNING! Skipping bins install: needs 'tools'"
+endif
+
.PHONY: install-headers
install-headers:
@echo "installing headers to $(DESTDIR)$(includedir)"
diff --git a/README.md b/README.md
index 78c4be0..fa8c9a8 100644
--- a/README.md
+++ b/README.md
@@ -31,6 +31,7 @@ Last Updated: 10 December 2024
- [Introduction](#introduction)
- [Prerequisites](#prerequisites)
- [Building RedisX](#building-redisx)
+ - [Command-line interface (`redisx-cli`)](#redisx-cli)
- [Linking your application against RedisX](#linking)
- [Managing Redis server connections](#managing-redis-server-connections)
- [Simple Redis queries](#simple-redis-queries)
@@ -144,6 +145,25 @@ Or, to stage the installation (to `/usr`) under a 'build root':
-----------------------------------------------------------------------------
+
+## Command-line interface (`redisx-cli`)
+
+The __RedisX__ library provides its own command-line tool, called `redisx-cli`. It works very similar to `redis-cli`,
+except that it supports a subset of the options (so far...).
+
+```bash
+ $ redisx-cli ping "Hello World"
+```
+will print:
+
+```bash
+ "Hello world!"
+```
+
+provided it successfully connected to the Redis / Valkey server. (Otherwise it will print an error and a trace).
+
+-----------------------------------------------------------------------------
+
## Linking your application against RedisX
diff --git a/build.mk b/build.mk
index 28a196d..01fc676 100644
--- a/build.mk
+++ b/build.mk
@@ -23,7 +23,12 @@ $(LIB)/lib%.so:
$(LIB)/%.a:
@make $(LIB)
ar -rc $@ $^
- ranlib $@
+ ranlib $@
+
+# Simple binaries
+$(BIN)/%: $(OBJ)/%.o
+ @make $(BIN)
+ $(CC) -o $@ $^ $(LDFLAGS)
# Create sub-directories for build targets
dep $(OBJ) $(LIB) $(BIN) apidoc:
diff --git a/include/redisx.h b/include/redisx.h
index ff8829a..d113ac8 100644
--- a/include/redisx.h
+++ b/include/redisx.h
@@ -169,7 +169,7 @@ enum redisx_protocol {
*/
typedef struct RESP {
enum resp_type type; ///< RESP type; RESP_ARRAY, RESP_INT ...
- int n; ///< Either the integer value of a RESP_INT or a RESP3_BOOLEAN response, or the
+ int n; ///< Either the integer value of a RESP_INT or a RESP3_BOOLEAN response, or the
///< dimension of the value field.
void *value; ///< Pointer to text (char *) content or to an array of components
///< (RESP**) or (RedisMapEntry *), or else a pointer to a `double`, depending
@@ -436,7 +436,8 @@ boolean redisxIsEqualRESP(const RESP *a, const RESP *b);
int redisxSplitText(RESP *resp, char **text);
XField *redisxRESP2XField(const char *name, const RESP *resp);
char *redisxRESP2JSON(const char *name, const RESP *resp);
-int redisxPrintRESP(const char *name, const RESP *resp);
+int redisxPrintRESP(const RESP *resp);
+int redisxPrintJSON(const char *name, const RESP *resp);
RedisMapEntry *redisxGetMapEntry(const RESP *map, const RESP *key);
RedisMapEntry *redisxGetKeywordEntry(const RESP *map, const char *key);
diff --git a/src/redisx-cli.c b/src/redisx-cli.c
new file mode 100644
index 0000000..b3924dc
--- /dev/null
+++ b/src/redisx-cli.c
@@ -0,0 +1,113 @@
+/**
+ * @date Created on Dec 11, 2024
+ * @author Attila Kovacs
+ */
+
+#define _POSIX_C_SOURCE 199309L ///< for nanosleep()
+
+#include
+#include
+#include
+#include
+#include
+
+#include "redisx.h"
+
+#define FORMAT_JSON 1
+#define FORMAT_RAW 2
+
+int main(int argc, const char *argv[]) {
+
+ char *host = "127.0.0.1";
+ int port = 0;
+ double timeout = 0.0;
+ char *password = NULL;
+ char *user = NULL;
+ int repeat = 1;
+ double interval = 1.0;
+ int dbIndex = 0;
+ int protocol = -1;
+ int format = 0;
+
+ struct poptOption options[] = { //
+ {"host", 'h', POPT_ARG_STRING, &host, 0, "Server hostname (default: 127.0.0.1).", NULL}, //
+ {"port", 'p', POPT_ARG_INT, &port, 0, "Server port (default: 6379).", NULL}, //
+ {"timeout", 't', POPT_ARG_DOUBLE, &timeout, 0, "Server connection timeout in seconds (decimals allowed).", NULL}, //
+ {"pass", 'a', POPT_ARG_STRING, &password, 0, "Password to use when connecting to the server.", NULL}, //
+ {"user", 'u', POPT_ARG_STRING, &user, 0, "Used to send ACL style 'AUTH username pass'. Needs -a.", NULL}, //
+ {"repeat", 'r', POPT_ARG_INT, &repeat, 0, "Execute specified command N times.", NULL}, //
+ {"interval", 'i', POPT_ARG_DOUBLE, &interval, 0, "When -r is used, waits seconds per command. " //
+ "It is possible to specify sub-second times like -i 0.1.", NULL}, //
+ {"db", 'n', POPT_ARG_INT, &dbIndex, 0, "Database number.", NULL}, //
+ {"resp2", '2', POPT_ARG_NONE, NULL, 2, "Start session in RESP2 protocol mode.", NULL}, //
+ {"resp3", '3', POPT_ARG_NONE, NULL, 3, "Start session in RESP3 protocol mode.", NULL}, //
+ {"json", 0, POPT_ARG_NONE, NULL, 'j', "Print raw strings", NULL}, //
+ POPT_AUTOHELP POPT_TABLEEND //
+ };
+
+ int rc;
+ char **cmdargs;
+ int i, nargs = 0;
+ Redis *redis;
+
+ poptContext optcon = poptGetContext("redisx-cli", argc, argv, options, 0);
+
+ while((rc = poptGetNextOpt(optcon)) != -1) {
+ if(rc < -1) {
+ fprintf(stderr, "ERROR! Bad syntax. Try running with --help to see command-line options.\n");
+ exit(1);
+ }
+
+ switch(rc) {
+ case '2': protocol = 2; break;
+ case '3': protocol = 3; break;
+ case 'j': format = FORMAT_JSON; break;
+ case 'r': format = FORMAT_RAW; break;
+ }
+ }
+
+ cmdargs = (char **) poptGetArgs(optcon);
+
+ if(!cmdargs) {
+ poptPrintHelp(optcon, stdout, 0);
+ return 1;
+ }
+
+ while(cmdargs[nargs]) nargs++;
+
+ redis = redisxInit(host);
+ if(port) redisxSetPort(redis, port);
+ if(user) redisxSetUser(redis, user);
+ if(password) redisxSetPassword(redis, password);
+ if(dbIndex > 0) redisxSelectDB(redis, dbIndex);
+ if(timeout > 0.0) redisxSetSocketTimeout(redis, (int) ceil(1000 * timeout));
+ if(protocol > 0) redisxSetProtocol(redis, protocol);
+
+ redisxConnect(redis, 0);
+ xSetDebug(1);
+
+ for(i = 0; i < repeat; i++) {
+ int status = X_SUCCESS;
+ RESP *reply;
+
+ if(i > 0 && interval > 0.0) {
+ struct timespec sleeptime;
+ sleeptime.tv_sec = (int) interval;
+ sleeptime.tv_nsec = 1000000000 * (interval - sleeptime.tv_sec);
+ nanosleep(&sleeptime, NULL);
+ }
+
+ reply = redisxArrayRequest(redis, cmdargs, NULL, nargs, &status);
+
+ if(!status) {
+ if(format == FORMAT_JSON) redisxPrintJSON("REPLY", reply);
+ else redisxPrintRESP(reply);
+ }
+
+ redisxDestroyRESP(reply);
+ }
+
+ poptFreeContext(optcon);
+
+ return 0;
+}
diff --git a/src/redisx-client.c b/src/redisx-client.c
index dca5bac..46ab105 100644
--- a/src/redisx-client.c
+++ b/src/redisx-client.c
@@ -942,7 +942,7 @@ RESP *redisxReadReplyAsync(RedisClient *cl) {
}
- for(i=0; in; i++) {
+ for(i = 0; i < resp->n; i++) {
RESP *r = redisxReadReplyAsync(cl); // Always read RESP even if we don't have storage for it...
if(component) component[i] = r;
else redisxDestroyRESP(r);
@@ -970,7 +970,7 @@ RESP *redisxReadReplyAsync(RedisClient *cl) {
dict = (RedisMapEntry *) calloc(resp->n, sizeof(RedisMapEntry));
x_check_alloc(dict);
- for(i=0; in; i++) {
+ for(i = 0; i < resp->n; i++) {
RedisMapEntry *e = &dict[i];
e->key = redisxReadReplyAsync(cl);
e->value = redisxReadReplyAsync(cl);
diff --git a/src/redisx-net.c b/src/redisx-net.c
index 5270c54..c3ea65f 100644
--- a/src/redisx-net.c
+++ b/src/redisx-net.c
@@ -557,7 +557,6 @@ static void rShutdownAsync() {
while(l != NULL) {
ServerLink *next = l->next;
redisxDestroy(l->redis);
- free(l);
l = next;
}
@@ -1127,7 +1126,7 @@ boolean redisxIsConnected(Redis *redis) {
*
*/
void *RedisPipelineListener(void *pRedis) {
- static int counter, lastError;
+ static long counter, lastError;
Redis *redis = (Redis *) pRedis;
RedisPrivate *p;
@@ -1187,7 +1186,7 @@ void *RedisPipelineListener(void *pRedis) {
} // <-- End of listener loop...
- xvprintf("Redis-X> Stopped processing pipeline responses (%d processed)...\n", counter);
+ xvprintf("Redis-X> Stopped processing pipeline responses (%ld processed)...\n", counter);
rConfigLock(redis);
// If we are the current listener thread, then mark the listener as disabled.
diff --git a/src/redisx-sub.c b/src/redisx-sub.c
index 98bdf98..72bfdcd 100644
--- a/src/redisx-sub.c
+++ b/src/redisx-sub.c
@@ -526,7 +526,7 @@ static void rNotifyConsumers(Redis *redis, char *pattern, char *channel, char *m
*
*/
void *RedisSubscriptionListener(void *pRedis) {
- static int counter, lastError;
+ static long counter, lastError;
Redis *redis = (Redis *) pRedis;
RedisPrivate *p;
@@ -585,7 +585,7 @@ void *RedisSubscriptionListener(void *pRedis) {
// Check for NULL component (all except last -- the payload -- which may be NULL)
for(i=reply->n-1; --i >= 0; ) if(component[i] == NULL) {
- fprintf(stderr, "WARNING! Redis-X : subscriber NULL in component %d.\n", (i+1));
+ fprintf(stderr, "WARNING! Redis-X : subscriber NULL in component %d\n", (i+1));
continue;
}
@@ -625,7 +625,7 @@ void *RedisSubscriptionListener(void *pRedis) {
rConfigUnlock(redis);
}
- xvprintf("Redis-X> Stopped processing subscriptions (%d processed)...\n", counter);
+ xvprintf("Redis-X> Stopped processing subscriptions (%ld processed)...\n", counter);
redisxDestroyRESP(reply);
diff --git a/src/redisx-tab.c b/src/redisx-tab.c
index a7bd5bc..8669c77 100644
--- a/src/redisx-tab.c
+++ b/src/redisx-tab.c
@@ -87,7 +87,7 @@ RedisEntry *redisxGetTable(Redis *redis, const char *table, int *n) {
else {
int i;
- for(i=0; in; i+=2) {
+ for(i = 0; i < reply->n; i += 2) {
RedisEntry *e = &entries[i];
RedisMapEntry *component = &dict[i];
e->key = component->key->value;
@@ -423,7 +423,7 @@ char **redisxGetKeys(Redis *redis, const char *table, int *n) {
if(names == NULL) fprintf(stderr, "WARNING! Redis-X : alloc pointers for %d keys: %s\n", reply->n, strerror(errno));
else {
int i;
- for(i=0; in; i++) {
+ for(i = 0; i < reply->n; i++) {
RESP **component = (RESP **) reply->value;
names[i] = (char *) component[i]->value;
@@ -527,7 +527,8 @@ char **redisxScanKeys(Redis *redis, const char *pattern, int *n, int *status) {
char **pCursor;
char **names = NULL;
char countArg[20];
- int args = 0, i, j, capacity = SCAN_INITIAL_STORE_CAPACITY;
+ int capacity = SCAN_INITIAL_STORE_CAPACITY;
+ int args = 0, i, j;
if(n == NULL) {
x_error(X_NULL, EINVAL, fn, "parameter 'n' is NULL");
@@ -633,13 +634,13 @@ char **redisxScanKeys(Redis *redis, const char *pattern, int *n, int *status) {
qsort(names, *n, sizeof(char *), compare_strings);
// Remove duplicates
- for(i=*n; --i > 0; ) if(!strcmp(names[i], names[i-1])) {
+ for(i = *n; --i > 0; ) if(!strcmp(names[i], names[i-1])) {
free(names[i]);
names[i] = NULL;
}
// Compact...
- for(i=0, j=0; i < *n; i++) if(names[i]) {
+ for(i = 0, j = 0; i < *n; i++) if(names[i]) {
if(i != j) names[j] = names[i];
j++;
}
@@ -692,7 +693,8 @@ RedisEntry *redisxScanTable(Redis *redis, const char *table, const char *pattern
RedisEntry *entries = NULL;
char *cmd[7] = {NULL}, countArg[20];
char **pCursor;
- int args= 0, i, j, capacity = SCAN_INITIAL_STORE_CAPACITY;
+ int capacity = SCAN_INITIAL_STORE_CAPACITY;
+ int args= 0, i, j;
if(n == NULL) {
x_error(X_NULL, EINVAL, fn, "parameter 'n' is NULL");
diff --git a/src/resp.c b/src/resp.c
index be0517c..133cc5c 100644
--- a/src/resp.c
+++ b/src/resp.c
@@ -427,7 +427,7 @@ int redisxAppendRESP(RESP *resp, RESP *part) {
extend = (char *) realloc(resp->value, (resp->n + part->n) * eSize);
if(!extend) {
free(old);
- return x_error(X_FAILURE, errno, fn, "alloc RESP array (%d components)", resp->n + part->n);
+ return x_error(X_FAILURE, errno, fn, "alloc RESP array (%ld components)", resp->n + part->n);
}
memcpy(extend + resp->n * eSize, part->value, part->n * eSize);
@@ -472,8 +472,8 @@ boolean redisxIsEqualRESP(const RESP *a, const RESP *b) {
* @sa redisxGetKeywordEntry()
*/
RedisMapEntry *redisxGetMapEntry(const RESP *map, const RESP *key) {
- int i;
RedisMapEntry *entries;
+ int i;
if(!key) return NULL;
if(!redisxIsMapType(map)) return NULL;
@@ -510,8 +510,8 @@ RedisMapEntry *redisxGetMapEntry(const RESP *map, const RESP *key) {
* @sa redisxGetMapEntry()
*/
RedisMapEntry *redisxGetKeywordEntry(const RESP *map, const char *key) {
- int i;
RedisMapEntry *entries;
+ int i;
if(!key) return NULL;
if(!redisxIsMapType(map)) return NULL;
@@ -566,66 +566,74 @@ static void rSetRESPType(XField *f, char type) {
}
static XField *respArrayToXField(const char *name, const RESP **component, int n) {
- static const char *fn = "respArrayToXField";
- XField *f;
enum resp_type type = RESP3_NULL;
+ XType eType;
+ XField *f;
+
int i;
- if(n < 0) return NULL;
+ if(n <= 0) return NULL;
for(i = 0; i < n; i++) {
if(i == 0) type = component[i]->type;
else if(component[i]->type != type) break;
}
- if(i < n) {
+ eType = resp2xType(type);
+
+ if(i < n || eType == X_FIELD) {
// --------------------------------------------------------
// Heterogeneous array...
XField *array = (XField *) calloc(n, sizeof(XField));
if(!array) {
- x_error(0, errno, fn, "alloc error (%d XField)", n);
+ x_error(0, errno, "respArrayToXField", "alloc error (%d XField)", n);
return NULL;
}
- f = xCreateMixed1DField(name, n, array);
-
for(i = 0; i < n; i++) {
- XField *e = redisxRESP2XField(array[i].name, component[i]);
+ XField *e;
+ char idx[20];
+ sprintf(idx, ".%d", (i + 1));
+ e = redisxRESP2XField(idx, component[i]);
if(e) {
array[i] = *e;
free(e);
}
}
+
+ f = xCreateMixed1DField(name, n, array);
}
else {
// --------------------------------------------------------
// Homogeneous array...
- XType eType = resp2xType(type);
char *array;
size_t eSize;
- if(eType == X_UNKNOWN) return xCreateMixed1DField(name, 0, NULL);
+ if(eType == X_UNKNOWN) eType = X_STRING;
eSize = xElementSizeOf(eType);
- array = (char *) calloc(1, n * eSize);
-
- f = xCreate1DField(name, eType, n, array);
- rSetRESPType(f, type);
- if(!array) return x_trace_null(fn, "field array");
+ array = (char *) calloc(n, eSize);
for(i = 0; i < n; i++) {
- XField *e = redisxRESP2XField("", component[i]);
+ XField *e;
+ char idx[20];
+
+ sprintf(idx, ".%d", (i + 1));
+ e = redisxRESP2XField(idx, component[i]);
if(e) {
- memcpy(&array[i * eSize], e, sizeof(XField));
+ memcpy(&array[i * eSize], e->value, eSize);
free(e);
}
}
+
+ f = xCreate1DField(name, eType, n, array);
}
+ rSetRESPType(f, type);
return f;
}
@@ -634,10 +642,14 @@ static XField *respMap2XField(const char *name, const RedisMapEntry *map, int n)
XStructure *s = xCreateStruct(), *nonstring = NULL;
int nNonString = 0;
+ printf("### map\n");
+
while(--n >= 0) {
const RedisMapEntry *e = &map[n];
if(redisxIsStringType(e->key)) {
+ printf("### + %s\n", (char *) e->key->value);
+
XField *fi = redisxRESP2XField((char *) e->key->value, e->value);
if(fi) {
fi->next = s->firstField;
@@ -654,7 +666,7 @@ static XField *respMap2XField(const char *name, const RedisMapEntry *map, int n)
sprintf(idx, ".%d", ++nNonString);
if(!nonstring)
nonstring = xCreateStruct();
- xSetSubstruct(nonstring, xStringCopyOf(idx), sub);
+ xSetSubstruct(nonstring, idx, sub);
}
}
@@ -687,13 +699,11 @@ static XField *respMap2XField(const char *name, const RedisMapEntry *map, int n)
* @sa redisxRESP2JSON()
*/
XField *redisxRESP2XField(const char *name, const RESP *resp) {
- static const char *fn = "resp2XField";
-
- errno = 0;
+ if(!resp) return NULL;
switch(resp->type) {
case RESP3_NULL:
- return xCreateStringField(name, NULL);
+ return xCreateScalarField(name, X_UNKNOWN, NULL);
case RESP3_BOOLEAN:
return xCreateBooleanField(name, resp->n);
@@ -719,7 +729,7 @@ XField *redisxRESP2XField(const char *name, const RESP *resp) {
case RESP3_SET:
case RESP3_PUSH: {
XField *f = respArrayToXField(name, (const RESP **) resp->value, resp->n);
- if(!f) return x_trace_null(fn, NULL);
+ if(!f) return NULL;
rSetRESPType(f, resp->type);
return f;
}
@@ -727,14 +737,14 @@ XField *redisxRESP2XField(const char *name, const RESP *resp) {
case RESP3_MAP:
case RESP3_ATTRIBUTE: {
XField *f = respMap2XField(name, (const RedisMapEntry *) resp->value, resp->n);
- if(!f) return x_trace_null(fn, NULL);
+ if(!f) return NULL;
rSetRESPType(f, resp->type);
return f;
}
}
- return NULL;
+ return xCreateStringField(name, "");
}
/**
@@ -746,7 +756,7 @@ XField *redisxRESP2XField(const char *name, const RESP *resp) {
* set to indicate the type of error).
*
* @sa redisxRESP2XField()
- * @sa redisxPrintRESP()
+ * @sa redisxPrintJSON()
*/
char *redisxRESP2JSON(const char *name, const RESP *resp) {
return xjsonFieldToString(redisxRESP2XField(name, resp));
@@ -759,9 +769,10 @@ char *redisxRESP2JSON(const char *name, const RESP *resp) {
* @param resp The RESP data to print
* @return 0
*
+ * @sa redisxPrintRESP()
* @sa redisxRESP2JSON()
*/
-int redisxPrintRESP(const char *name, const RESP *resp) {
+int redisxPrintJSON(const char *name, const RESP *resp) {
char *json = redisxRESP2JSON(name, resp);
if(json) {
@@ -773,3 +784,104 @@ int redisxPrintRESP(const char *name, const RESP *resp) {
return X_SUCCESS;
}
+static void rNewLine(int ident) {
+ putchar('\n');
+ while(--ident >= 0) putchar(' ');
+}
+
+static int rIndexWidth(int count) {
+ return 1 + (int) floor(log10(count));
+}
+
+static int rPrintRESP(int ident, const RESP *resp) {
+
+ if(!resp) {
+ printf("(null)");
+ return X_SUCCESS;
+ }
+
+ if(resp->type == RESP3_NULL) {
+ printf("null");
+ return X_SUCCESS;
+ }
+
+ if(redisxIsStringType(resp)) {
+ int quote = !(resp->type == RESP_SIMPLE_STRING || resp->type == RESP_ERROR || resp->type == RESP3_BIG_NUMBER);
+
+ if(!resp->value) printf("(nil)");
+ else {
+ if(quote) putchar('"');
+ printf("%s", ((char *) resp->value));
+ if(quote) putchar('"');
+ }
+ return X_SUCCESS;
+ }
+
+ switch(resp->type) {
+ case RESP_INT:
+ printf("(integer) %d", resp->n);
+ return X_SUCCESS;
+
+ case RESP3_DOUBLE:
+ printf("(double) %g", *(double *) resp->value);
+ return X_SUCCESS;
+
+ case RESP_ARRAY:
+ case RESP3_SET:
+ case RESP3_PUSH: {
+ RESP **component = (RESP **) resp->value;
+
+ if(!resp->value) printf("(empty array)");
+ else {
+ int i;
+ for(i = 0; i < resp->n; i++) {
+ rNewLine(ident);
+ printf("%*d) ", rIndexWidth(resp->n), (i + 1));
+ rPrintRESP(ident + 2, component[i]);
+ }
+ }
+ return X_SUCCESS;
+ }
+
+ case RESP3_MAP:
+ case RESP3_ATTRIBUTE: {
+ const RedisMapEntry *component = (RedisMapEntry *) resp->value;
+
+ if(!resp->value) printf("(empty map)");
+ else {
+ int i;
+ for(i = 0; i < resp->n; i++) {
+ rNewLine(ident);
+ printf("%*d# ", rIndexWidth(resp->n), (i + 1));
+ rPrintRESP(ident + 2, component[i].key);
+ printf(" => ");
+ rPrintRESP(ident + 2, component[i].value);
+ }
+ }
+ return X_SUCCESS;
+ }
+
+ default:
+ if(!resp->value) printf("(nil)");
+ else printf(" type '%c'", resp->type);
+ return X_FAILURE;
+ }
+
+
+}
+
+/**
+ * Prints a RESP to the standard output, in a format that is similar to the one used by the standard
+ * redis-cli tool.
+ *
+ * @param resp Pointer to a RESP data structure. (It may be NULL).
+ * @return X_SUCCESS (0) if successful or else X_FAILURE if there was an error.
+ *
+ * @sa redisxPrintJSON()
+ */
+int redisxPrintRESP(const RESP *resp) {
+ int status = rPrintRESP(0, resp);
+ printf("\n");
+ return status;
+}
+
diff --git a/test/src/test-tab.c b/test/src/test-tab.c
index 2f9702e..ace965e 100644
--- a/test/src/test-tab.c
+++ b/test/src/test-tab.c
@@ -41,7 +41,7 @@ int main() {
return 1;
}
- redisxPrintRESP("get value", resp);
+ redisxPrintJSON("get value", resp);
redisxCheckDestroyRESP(resp, RESP_BULK_STRING, 1);
if(strcmp("2", (char *) resp->value) != 0) {
diff --git a/tools.mk b/tools.mk
new file mode 100644
index 0000000..4f5eb5b
--- /dev/null
+++ b/tools.mk
@@ -0,0 +1,9 @@
+include config.mk
+
+LDFLAGS += -L$(LIB) -lredisx -lpopt
+LD_LIBRARY_PATH := $(LIB):$(LD_LIBRARY_PATH)
+
+# Top level make targets...
+all: $(BIN)/redisx-cli
+
+include build.mk
\ No newline at end of file