Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions common/redisreply.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <sstream>
#include <functional>
#include <boost/algorithm/string.hpp>
#include <nlohmann/json.hpp>

#include "common/logger.h"
#include "common/redisreply.h"
Expand Down Expand Up @@ -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())
Expand Down
2 changes: 2 additions & 0 deletions common/redisreply.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
24 changes: 19 additions & 5 deletions sonic-db-cli/sonic-db-cli.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -133,7 +134,8 @@ int executeCommands(
const string& db_name,
vector<string>& commands,
const string& netns,
bool useUnixSocket)
bool useUnixSocket,
bool useJson)
{
shared_ptr<DBConnector> client = nullptr;
try
Expand Down Expand Up @@ -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)
{
Expand All @@ -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", 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 }
Expand All @@ -211,6 +221,10 @@ void parseCliArguments(
options.m_unixsocket = true;
break;

case 'j':
options.m_json = true;
break;

case 'n':
if (optind < argc)
{
Expand Down Expand Up @@ -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"
Expand Down
4 changes: 3 additions & 1 deletion sonic-db-cli/sonic-db-cli.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string> m_cmd;
Expand All @@ -30,7 +31,8 @@ int executeCommands(
const std::string& db_name,
std::vector<std::string>& commands,
const std::string& netns,
bool isTcpConn);
bool isTcpConn,
bool useJson = false);

std::string handleSingleOperation(
const std::string& netns,
Expand Down
3 changes: 2 additions & 1 deletion tests/cli_test_data/cli_help_output.txt
Original file line number Diff line number Diff line change
@@ -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:

Expand All @@ -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

Expand Down
77 changes: 77 additions & 0 deletions tests/cli_ut.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
Loading