From 02427a4f16c931862a68cccfe5abed7ab1948267 Mon Sep 17 00:00:00 2001 From: Brian Gallagher Date: Tue, 9 Jun 2026 21:25:36 +0000 Subject: [PATCH 1/2] Support --json option in sonic-db-cli CLI Signed-off-by: Brian Gallagher --- common/redisreply.cpp | 75 ++++++++++++++++++++++++ common/redisreply.h | 2 + sonic-db-cli/sonic-db-cli.cpp | 24 ++++++-- sonic-db-cli/sonic-db-cli.h | 4 +- tests/cli_test_data/cli_help_output.txt | 3 +- tests/cli_ut.cpp | 77 +++++++++++++++++++++++++ 6 files changed, 178 insertions(+), 7 deletions(-) diff --git a/common/redisreply.cpp b/common/redisreply.cpp index 90cb77c16..ea21abea8 100644 --- a/common/redisreply.cpp +++ b/common/redisreply.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include "common/logger.h" #include "common/redisreply.h" @@ -291,6 +292,80 @@ string RedisReply::to_string(redisReply *reply, string command) } } +static nlohmann::ordered_json buildJsonDict(struct redisReply **element, size_t elements) +{ + if (elements%2 != 0) + { + throw system_error(make_error_code(errc::io_error), + "Invalid result"); + } + + auto result = nlohmann::ordered_json::object(); + for (unsigned int i = 0; i < elements; i += 2) + { + result[RedisReply::to_string(element[i])] = RedisReply::to_string(element[i + 1]); + } + + return result; +} + +static nlohmann::ordered_json buildJsonReply(redisReply *reply, const string& command) +{ + switch(reply->type) + { + case REDIS_REPLY_INTEGER: + return reply->integer; + + case REDIS_REPLY_NIL: + return nullptr; + + case REDIS_REPLY_STRING: + case REDIS_REPLY_ERROR: + case REDIS_REPLY_STATUS: + return string(reply->str, reply->len); + + case REDIS_REPLY_ARRAY: + { + if (command == "HGETALL") + { + return buildJsonDict(reply->element, reply->elements); + } + + if (command == "HSCAN" && reply->elements == 2) + { + auto result = nlohmann::ordered_json::array(); + result.push_back(buildJsonReply(reply->element[0], string())); + result.push_back(buildJsonDict(reply->element[1]->element, reply->element[1]->elements)); + return result; + } + + auto result = nlohmann::ordered_json::array(); + for (unsigned int i = 0; i < reply->elements; i++) + { + result.push_back(buildJsonReply(reply->element[i], string())); + } + return result; + } + + default: + SWSS_LOG_ERROR("invalid type %d for message", reply->type); + return nullptr; + } +} + +string RedisReply::to_json_string(redisReply *reply, string command) +{ + /* + JSON output mode for sonic-db-cli --json. + Unlike to_string(), this does not emulate redis-py output: replies map + directly to JSON types (integer -> number, nil -> null, status/error -> string), + except that HGETALL (and the field map element of HSCAN) become a JSON object. + Bytes that are not valid UTF-8 are replaced with U+FFFD rather than failing. + */ + auto result = buildJsonReply(reply, command); + return result.dump(-1, ' ', false, nlohmann::ordered_json::error_handler_t::replace); +} + string RedisReply::formatReply(string command, long long integer) { if (g_intToBoolCommands.find(command) != g_intToBoolCommands.end()) diff --git a/common/redisreply.h b/common/redisreply.h index ddb8e1ed2..7b304fe88 100644 --- a/common/redisreply.h +++ b/common/redisreply.h @@ -98,6 +98,8 @@ class RedisReply static std::string to_string(redisReply *reply, std::string command = std::string()); + static std::string to_json_string(redisReply *reply, std::string command = std::string()); + private: void checkStatus(const char *status); void checkReply(); diff --git a/sonic-db-cli/sonic-db-cli.cpp b/sonic-db-cli/sonic-db-cli.cpp index 8f9711506..cfa920793 100755 --- a/sonic-db-cli/sonic-db-cli.cpp +++ b/sonic-db-cli/sonic-db-cli.cpp @@ -12,7 +12,7 @@ using namespace std; void printUsage() { - cout << "usage: sonic-db-cli [-h] [-s] [-n NAMESPACE] db_or_op [cmd [cmd ...]]" << endl; + cout << "usage: sonic-db-cli [-h] [-s] [-j] [-n NAMESPACE] db_or_op [cmd [cmd ...]]" << endl; cout << endl; cout << "SONiC DB CLI:" << endl; cout << endl; @@ -23,6 +23,7 @@ void printUsage() cout << "optional arguments:" << endl; cout << " -h, --help show this help message and exit" << endl; cout << " -s, --unixsocket Override use of tcp_port and use unixsocket" << endl; + cout << " -j, --json Print command result as JSON" << endl; cout << " -n NAMESPACE, --namespace NAMESPACE" << endl; cout << " Namespace string to use asic0/asic1.../asicn" << endl; cout << endl; @@ -133,7 +134,8 @@ int executeCommands( const string& db_name, vector& commands, const string& netns, - bool useUnixSocket) + bool useUnixSocket, + bool useJson) { shared_ptr client = nullptr; try @@ -169,7 +171,14 @@ int executeCommands( with these changes, it is enough for us to mimic redis-cli in SONiC so far since no application uses tty mode redis-cli output */ auto commandName = getCommandName(commands); - cout << RedisReply::to_string(reply.getContext(), commandName) << endl; + if (useJson) + { + cout << RedisReply::to_json_string(reply.getContext(), commandName) << endl; + } + else + { + cout << RedisReply::to_string(reply.getContext(), commandName) << endl; + } } catch (const std::system_error& e) { @@ -186,10 +195,11 @@ void parseCliArguments( Options &options) { // Parse argument with getopt https://man7.org/linux/man-pages/man3/getopt.3.html - const char* short_options = "hsn"; + const char* short_options = "hsjn"; static struct option long_options[] = { {"help", optional_argument, NULL, 'h' }, {"unixsocket", optional_argument, NULL, 's' }, + {"json", optional_argument, NULL, 'j' }, {"namespace", optional_argument, NULL, 'n' }, // The last element of the array has to be filled with zeros. {0, 0, 0, 0 } @@ -211,6 +221,10 @@ void parseCliArguments( options.m_unixsocket = true; break; + case 'j': + options.m_json = true; + break; + case 'n': if (optind < argc) { @@ -298,7 +312,7 @@ int sonic_db_cli( initializeConfig(); } - return executeCommands(dbOrOperation, commands, netns, useUnixSocket); + return executeCommands(dbOrOperation, commands, netns, useUnixSocket, options.m_json); } else if (dbOrOperation == "PING" || dbOrOperation == "SAVE" diff --git a/sonic-db-cli/sonic-db-cli.h b/sonic-db-cli/sonic-db-cli.h index d1d874f8d..a5ed2fbc4 100755 --- a/sonic-db-cli/sonic-db-cli.h +++ b/sonic-db-cli/sonic-db-cli.h @@ -12,6 +12,7 @@ struct Options { bool m_help = false; bool m_unixsocket = false; + bool m_json = false; std::string m_namespace; std::string m_db_or_op; std::vector m_cmd; @@ -30,7 +31,8 @@ int executeCommands( const std::string& db_name, std::vector& commands, const std::string& netns, - bool isTcpConn); + bool isTcpConn, + bool useJson = false); std::string handleSingleOperation( const std::string& netns, diff --git a/tests/cli_test_data/cli_help_output.txt b/tests/cli_test_data/cli_help_output.txt index 1d286df17..fa078088a 100755 --- a/tests/cli_test_data/cli_help_output.txt +++ b/tests/cli_test_data/cli_help_output.txt @@ -1,4 +1,4 @@ -usage: sonic-db-cli [-h] [-s] [-n NAMESPACE] db_or_op [cmd [cmd ...]] +usage: sonic-db-cli [-h] [-s] [-j] [-n NAMESPACE] db_or_op [cmd [cmd ...]] SONiC DB CLI: @@ -9,6 +9,7 @@ positional arguments: optional arguments: -h, --help show this help message and exit -s, --unixsocket Override use of tcp_port and use unixsocket + -j, --json Print command result as JSON -n NAMESPACE, --namespace NAMESPACE Namespace string to use asic0/asic1.../asicn diff --git a/tests/cli_ut.cpp b/tests/cli_ut.cpp index bde4e3ebf..9ebdd61eb 100755 --- a/tests/cli_ut.cpp +++ b/tests/cli_ut.cpp @@ -147,6 +147,83 @@ TEST(sonic_db_cli, test_cli_hscan_commands) EXPECT_EQ("{'testfield': \"{'value': 'with qute'}\"}\n", output); } +TEST(sonic_db_cli, test_cli_json_output) +{ + char *args[7]; + args[0] = "sonic-db-cli"; + args[1] = "-j"; + args[2] = "TEST_DB"; + + // clear database, status reply becomes a JSON string + args[3] = "FLUSHDB"; + auto output = runCli(4, args); + EXPECT_EQ("\"OK\"\n", output); + + // hset to test DB, integer reply becomes a JSON number + args[3] = "HSET"; + args[4] = "testkey"; + args[5] = "testfield"; + args[6] = "testvalue"; + output = runCli(7, args); + EXPECT_EQ("1\n", output); + + // hgetall from test db becomes a JSON object + args[3] = "HGETALL"; + args[4] = "testkey"; + output = runCli(5, args); + EXPECT_EQ("{\"testfield\":\"testvalue\"}\n", output); + + // hgetall of a missing key becomes an empty JSON object + args[3] = "HGETALL"; + args[4] = "notexistkey"; + output = runCli(5, args); + EXPECT_EQ("{}\n", output); + + // hscan from test db, cursor is a JSON string and fields a JSON object + args[3] = "HSCAN"; + args[4] = "testkey"; + args[5] = "0"; + output = runCli(6, args); + EXPECT_EQ("[\"0\",{\"testfield\":\"testvalue\"}]\n", output); + + // values with quote characters are escaped + args[3] = "HSET"; + args[4] = "quotekey"; + args[5] = "testfield"; + args[6] = "va'l\"ue"; + output = runCli(7, args); + EXPECT_EQ("1\n", output); + + args[3] = "HGETALL"; + args[4] = "quotekey"; + output = runCli(5, args); + EXPECT_EQ("{\"testfield\":\"va'l\\\"ue\"}\n", output); + + // get from test db becomes a quoted JSON string + args[3] = "GET"; + args[4] = "notexistkey"; + output = runCli(5, args); + EXPECT_EQ("null\n", output); + + // keys from test db becomes a JSON array + args[3] = "DEL"; + args[4] = "quotekey"; + output = runCli(5, args); + EXPECT_EQ("1\n", output); + + args[3] = "keys"; + args[4] = "*"; + output = runCli(5, args); + EXPECT_EQ("[\"testkey\"]\n", output); + + // long option works as well + args[1] = "--json"; + args[3] = "HGETALL"; + args[4] = "testkey"; + output = runCli(5, args); + EXPECT_EQ("{\"testfield\":\"testvalue\"}\n", output); +} + TEST(sonic_db_cli, test_cli_pop_commands) { char *args[10]; From 2090c489984c0cb2d5e027b2ef1905990bd2400f Mon Sep 17 00:00:00 2001 From: Brian Gallagher Date: Fri, 12 Jun 2026 09:42:08 -0700 Subject: [PATCH 2/2] Register json as no_argument Signed-off-by: Brian Gallagher --- sonic-db-cli/sonic-db-cli.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sonic-db-cli/sonic-db-cli.cpp b/sonic-db-cli/sonic-db-cli.cpp index cfa920793..f92df6197 100755 --- a/sonic-db-cli/sonic-db-cli.cpp +++ b/sonic-db-cli/sonic-db-cli.cpp @@ -199,7 +199,7 @@ void parseCliArguments( static struct option long_options[] = { {"help", optional_argument, NULL, 'h' }, {"unixsocket", optional_argument, NULL, 's' }, - {"json", optional_argument, NULL, 'j' }, + {"json", no_argument, NULL, 'j' }, {"namespace", optional_argument, NULL, 'n' }, // The last element of the array has to be filled with zeros. {0, 0, 0, 0 }