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..8e5d395 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`,
+expect that it prints replies in JSON format by default (but also supports `--raw` flag for string-type replies).
+
+```bash
+ $ redisx-cli ping "Hello World"
+```
+will print:
+
+```bash
+ "REPLY": "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/src/redisx-cli.c b/src/redisx-cli.c
new file mode 100644
index 0000000..d840192
--- /dev/null
+++ b/src/redisx-cli.c
@@ -0,0 +1,108 @@
+/**
+ * @file
+ *
+ * @date Created on Dec 11, 2024
+ * @author Attila Kovacs
+ */
+
+#include
+#include
+#include
+#include
+#include
+
+#include "redisx.h"
+
+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 raw = 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}, //
+ {"raw", 0, POPT_ARG_NONE, &raw, 0, "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;
+ }
+ }
+
+ 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;
+ 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(redisxIsStringType(reply) && raw) puts((char *) reply->value);
+ else redisxPrintRESP("REPLY", reply);
+ }
+
+ redisxDestroyRESP(reply);
+ }
+
+ poptFreeContext(optcon);
+
+ return 0;
+}
diff --git a/src/redisx-net.c b/src/redisx-net.c
index 5270c54..66ae7c1 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;
}
diff --git a/src/resp.c b/src/resp.c
index be0517c..0e9918b 100644
--- a/src/resp.c
+++ b/src/resp.c
@@ -592,7 +592,10 @@ static XField *respArrayToXField(const char *name, const RESP **component, int n
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);
@@ -611,19 +614,25 @@ static XField *respArrayToXField(const char *name, const RESP **component, int n
if(eType == X_UNKNOWN) return xCreateMixed1DField(name, 0, NULL);
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);
+ if(!array) return x_trace_null(fn, "field array");
+
+
}
return f;
@@ -654,7 +663,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);
}
}
@@ -765,7 +774,7 @@ int redisxPrintRESP(const char *name, const RESP *resp) {
char *json = redisxRESP2JSON(name, resp);
if(json) {
- printf("%s", json);
+ puts(json);
free(json);
}
else printf("\"%s\": null\n", name);
@@ -773,3 +782,4 @@ int redisxPrintRESP(const char *name, const RESP *resp) {
return X_SUCCESS;
}
+
diff --git a/test/src/test-hello.c b/test/src/test-hello.c
index f0fe224..ada8f27 100644
--- a/test/src/test-hello.c
+++ b/test/src/test-hello.c
@@ -33,7 +33,7 @@ int main() {
resp = redisxGetHelloData(redis);
json = redisxRESP2JSON("server_properties", resp);
- printf("%s", json ? json : "");
+ puts(json ? json : "");
free(json);
redisxDestroyRESP(resp);
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