1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
|
#!/usr/bin/env python3
import argparse
import json
import os
import subprocess
from collections import OrderedDict
from sys import argv
def convert_flags_to_boolean_dict(flags):
"""Return a dict with a key set to `True` per element in the flags list."""
return {f: True for f in flags}
def set_if_not_none_or_empty(dst, key, value):
"""Set 'key' in 'dst' if 'value' is not `None` or an empty list."""
if value is not None and (type(value) is not list or len(value)):
dst[key] = value
def convert_argument(arg):
"""Transform an argument."""
arg.update(convert_flags_to_boolean_dict(arg.pop('flags', [])))
set_if_not_none_or_empty(arg, 'arguments',
[convert_argument(x) for x in arg.pop('arguments', [])])
return arg
def convert_keyspec(spec):
"""Transform a key spec."""
spec.update(convert_flags_to_boolean_dict(spec.pop('flags', [])))
return spec
def convert_entry_to_objects_array(cmd, docs):
"""Transform the JSON output of `COMMAND` to a friendlier format.
cmd is the output of `COMMAND` as follows:
1. Name (lower case, e.g. "lolwut")
2. Arity
3. Flags
4-6. First/last/step key specification (deprecated as of Redis v7.0)
7. ACL categories
8. hints (as of Redis 7.0)
9. key-specs (as of Redis 7.0)
10. subcommands (as of Redis 7.0)
docs is the output of `COMMAND DOCS`, which holds a map of additional metadata
This returns a list with a dict for the command and per each of its
subcommands. Each dict contains one key, the command's full name, with a
value of a dict that's set with the command's properties and meta
information."""
assert len(cmd) >= 9
obj = {}
rep = [obj]
name = cmd[0].upper()
arity = cmd[1]
command_flags = cmd[2]
acl_categories = cmd[6]
hints = cmd[7]
keyspecs = cmd[8]
subcommands = cmd[9] if len(cmd) > 9 else []
key = name.replace('|', ' ')
subcommand_docs = docs.pop('subcommands', [])
rep.extend([convert_entry_to_objects_array(x, subcommand_docs[x[0]])[0] for x in subcommands])
# The command's value is ordered so the interesting stuff that we care about
# is at the start. Optional `None` and empty list values are filtered out.
value = OrderedDict()
value['summary'] = docs.pop('summary')
value['since'] = docs.pop('since')
value['group'] = docs.pop('group')
set_if_not_none_or_empty(value, 'complexity', docs.pop('complexity', None))
set_if_not_none_or_empty(value, 'deprecated_since', docs.pop('deprecated_since', None))
set_if_not_none_or_empty(value, 'replaced_by', docs.pop('replaced_by', None))
set_if_not_none_or_empty(value, 'history', docs.pop('history', []))
set_if_not_none_or_empty(value, 'acl_categories', acl_categories)
value['arity'] = arity
set_if_not_none_or_empty(value, 'key_specs',
[convert_keyspec(x) for x in keyspecs])
set_if_not_none_or_empty(value, 'arguments',
[convert_argument(x) for x in docs.pop('arguments', [])])
set_if_not_none_or_empty(value, 'command_flags', command_flags)
set_if_not_none_or_empty(value, 'doc_flags', docs.pop('doc_flags', []))
set_if_not_none_or_empty(value, 'hints', hints)
# All remaining docs key-value tuples, if any, are appended to the command
# to be future-proof.
while len(docs) > 0:
(k, v) = docs.popitem()
value[k] = v
obj[key] = value
return rep
# Figure out where the sources are
srcdir = os.path.abspath(os.path.dirname(os.path.abspath(__file__)) + "/../src")
# MAIN
if __name__ == '__main__':
opts = {
'description': 'Transform the output from `redis-cli --json` using COMMAND and COMMAND DOCS to a single commands.json format.',
'epilog': f'Usage example: {argv[0]} --cli src/redis-cli --port 6379 > commands.json'
}
parser = argparse.ArgumentParser(**opts)
parser.add_argument('--host', type=str, default='localhost')
parser.add_argument('--port', type=int, default=6379)
parser.add_argument('--cli', type=str, default='%s/redis-cli' % srcdir)
args = parser.parse_args()
payload = OrderedDict()
cmds = []
p = subprocess.Popen([args.cli, '-h', args.host, '-p', str(args.port), '--json', 'command'], stdout=subprocess.PIPE)
stdout, stderr = p.communicate()
commands = json.loads(stdout)
p = subprocess.Popen([args.cli, '-h', args.host, '-p', str(args.port), '--json', 'command', 'docs'],
stdout=subprocess.PIPE)
stdout, stderr = p.communicate()
docs = json.loads(stdout)
for entry in commands:
cmd = convert_entry_to_objects_array(entry, docs[entry[0]])
cmds.extend(cmd)
# The final output is a dict of all commands, ordered by name.
cmds.sort(key=lambda x: list(x.keys())[0])
for cmd in cmds:
name = list(cmd.keys())[0]
payload[name] = cmd[name]
print(json.dumps(payload, indent=4))
|