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