Skip to content

Commit 0e162b1

Browse files
SYN-6647: tab completion in storm CLI (#3493)
feat: Add tab completion in Storm CLI. Completes forms, properties, libs and tags.
1 parent 98af04d commit 0e162b1

File tree

4 files changed

+354
-9
lines changed

4 files changed

+354
-9
lines changed

synapse/cortex.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5474,10 +5474,26 @@ async def getStormDocs(self):
54745474
dict: A Dictionary of storm documentation information.
54755475
'''
54765476

5477+
cmds = []
5478+
5479+
for name, cmd in self.stormcmds.items():
5480+
entry = {
5481+
'name': name,
5482+
'doc': cmd.getCmdBrief(),
5483+
}
5484+
5485+
if cmd.pkgname:
5486+
entry['package'] = cmd.pkgname
5487+
5488+
if cmd.svciden:
5489+
entry['svciden'] = cmd.svciden
5490+
5491+
cmds.append(entry)
5492+
54775493
ret = {
54785494
'libraries': s_stormtypes.registry.getLibDocs(),
54795495
'types': s_stormtypes.registry.getTypeDocs(),
5480-
# 'cmds': ... # TODO - support cmd docs
5496+
'commands': cmds,
54815497
# 'packages': ... # TODO - Support inline information for packages?
54825498
}
54835499
return ret

synapse/lib/cli.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,8 @@ async def __anit__(self, item, outp=None, **locs):
248248
self.echoline = False
249249
self.colorsenabled = False
250250

251+
self.completer = None
252+
251253
if isinstance(self.item, s_base.Base):
252254
self.item.onfini(self._onItemFini)
253255

@@ -312,7 +314,12 @@ async def prompt(self, text=None):
312314
except OSError: # pragma: no cover
313315
logger.warning(f'Unable to create file at {histfp}, cli history will not be stored.')
314316

315-
self.sess = PromptSession(history=history)
317+
self.sess = PromptSession(
318+
history=history,
319+
completer=self.completer,
320+
complete_while_typing=False,
321+
reserve_space_for_menu=5,
322+
)
316323

317324
if text is None:
318325
text = self.cmdprompt

synapse/tests/test_tools_storm.py

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import os
2-
import subprocess
32
import synapse.tests.utils as s_test
43

4+
from prompt_toolkit.document import Document
5+
from prompt_toolkit.completion import Completion, CompleteEvent
6+
57
import synapse.exc as s_exc
68
import synapse.common as s_common
79
import synapse.lib.output as s_output
@@ -206,3 +208,117 @@ async def test_tools_storm_view(self):
206208
outp = s_output.OutPutStr()
207209
await s_t_storm.main(('--optsfile', optsfile, url, 'file:bytes'), outp=outp)
208210
self.isin('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', str(outp))
211+
212+
async def test_storm_tab_completion(self):
213+
class DummyStorm:
214+
def __init__(self, core):
215+
self.item = core
216+
217+
async with self.getTestCore() as core:
218+
cli = DummyStorm(core)
219+
220+
completer = s_t_storm.StormCompleter(cli)
221+
222+
async def get_completions(text):
223+
document = Document(text)
224+
event = CompleteEvent(completion_requested=True)
225+
return await s_test.alist(completer.get_completions_async(document, event))
226+
227+
vals = await get_completions('')
228+
self.len(0, vals)
229+
230+
# Check completion of forms/props
231+
vals = await get_completions('inet:fq')
232+
self.isin(Completion('dn', display='[form] inet:fqdn - A Fully Qualified Domain Name (FQDN).'), vals)
233+
self.isin(Completion('dn.seen', display='[prop] inet:fqdn.seen - The time interval for first/last observation of the node.'), vals)
234+
self.isin(Completion('dn.created', display='[prop] inet:fqdn.created - The time the node was created in the cortex.'), vals)
235+
self.isin(Completion('dn:domain', display='[prop] inet:fqdn:domain - The parent domain for the FQDN.'), vals)
236+
self.isin(Completion('dn:host', display='[prop] inet:fqdn:host - The host part of the FQDN.'), vals)
237+
self.isin(Completion('dn:issuffix', display='[prop] inet:fqdn:issuffix - True if the FQDN is considered a suffix.'), vals)
238+
self.isin(Completion('dn:iszone', display='[prop] inet:fqdn:iszone - True if the FQDN is considered a zone.'), vals)
239+
self.isin(Completion('dn:zone', display='[prop] inet:fqdn:zone - The zone level parent for this FQDN.'), vals)
240+
241+
vals = await get_completions('inet:fqdn.')
242+
self.isin(Completion('seen', display='[prop] inet:fqdn.seen - The time interval for first/last observation of the node.'), vals)
243+
self.isin(Completion('created', display='[prop] inet:fqdn.created - The time the node was created in the cortex.'), vals)
244+
245+
vals = await get_completions('[inet:fq')
246+
self.isin(Completion('dn', display='[form] inet:fqdn - A Fully Qualified Domain Name (FQDN).'), vals)
247+
self.isin(Completion('dn.seen', display='[prop] inet:fqdn.seen - The time interval for first/last observation of the node.'), vals)
248+
249+
vals = await get_completions('[inet:')
250+
self.isin(Completion('fqdn', display='[form] inet:fqdn - A Fully Qualified Domain Name (FQDN).'), vals)
251+
self.isin(Completion('ipv4', display='[form] inet:ipv4 - An IPv4 address.'), vals)
252+
253+
# No tags to return
254+
vals = await get_completions('inet:ipv4#')
255+
self.len(0, vals)
256+
257+
# Add some tags
258+
await core.stormlist('[inet:ipv4=1.2.3.4 +#rep.foo]')
259+
await core.stormlist('[inet:ipv4=1.2.3.5 +#rep.foo.bar]')
260+
await core.stormlist('[inet:ipv4=1.2.3.6 +#rep.bar]')
261+
await core.stormlist('[inet:ipv4=1.2.3.7 +#rep.baz]')
262+
await core.stormlist('[syn:tag=rep :doc="Reputation base."]')
263+
264+
# Check completion of tags
265+
vals = await get_completions('inet:ipv4#')
266+
self.len(4, vals)
267+
self.isin(Completion('rep', display='[tag] rep - Reputation base.'), vals)
268+
self.isin(Completion('rep.foo', display='[tag] rep.foo'), vals)
269+
self.isin(Completion('rep.bar', display='[tag] rep.bar'), vals)
270+
self.isin(Completion('rep.baz', display='[tag] rep.baz'), vals)
271+
272+
vals = await get_completions('inet:ipv4#rep.')
273+
self.len(4, vals)
274+
self.isin(Completion('foo', display='[tag] rep.foo'), vals)
275+
self.isin(Completion('foo.bar', display='[tag] rep.foo.bar'), vals)
276+
self.isin(Completion('bar', display='[tag] rep.bar'), vals)
277+
self.isin(Completion('baz', display='[tag] rep.baz'), vals)
278+
279+
vals = await get_completions('inet:ipv4 +#')
280+
self.isin(Completion('rep.foo', display='[tag] rep.foo'), vals)
281+
282+
vals = await get_completions('inet:ipv4 -#')
283+
self.isin(Completion('rep.foo', display='[tag] rep.foo'), vals)
284+
285+
vals = await get_completions('[inet:ipv4 +#')
286+
self.isin(Completion('rep.foo', display='[tag] rep.foo'), vals)
287+
288+
vals = await get_completions('inet:ipv4 { +#')
289+
self.isin(Completion('rep.foo', display='[tag] rep.foo'), vals)
290+
291+
# Check completion of cmds
292+
vals = await get_completions('vau')
293+
self.isin(Completion('lt.add', display='[cmd] vault.add - Add a vault.'), vals)
294+
self.isin(Completion('lt.set.secrets', display='[cmd] vault.set.secrets - Set vault secret data.'), vals)
295+
self.isin(Completion('lt.set.configs', display='[cmd] vault.set.configs - Set vault config data.'), vals)
296+
self.isin(Completion('lt.del', display='[cmd] vault.del - Delete a vault.'), vals)
297+
self.isin(Completion('lt.list', display='[cmd] vault.list - List available vaults.'), vals)
298+
self.isin(Completion('lt.set.perm', display='[cmd] vault.set.perm - Set permissions on a vault.'), vals)
299+
300+
vals = await get_completions('inet:ipv4 +#rep.foo | ser')
301+
self.isin(Completion('vice.add', display='[cmd] service.add - Add a storm service to the cortex.'), vals)
302+
self.isin(Completion('vice.del', display='[cmd] service.del - Remove a storm service from the cortex.'), vals)
303+
self.isin(Completion('vice.list', display='[cmd] service.list - List the storm services configured in the cortex.'), vals)
304+
305+
# Check completion of libs
306+
vals = await get_completions('inet:ipv4 $li')
307+
self.len(0, vals)
308+
309+
vals = await get_completions('inet:ipv4 $lib')
310+
self.isin(
311+
Completion(
312+
'.auth.easyperm.allowed',
313+
display='[lib] $lib.auth.easyperm.allowed(edef: dict, level: str) - Check if the current user has a permission level in an easy perm dictionary.'
314+
),
315+
vals
316+
)
317+
318+
self.isin(
319+
Completion(
320+
'.vault.list',
321+
display='[lib] $lib.vault.list() - List vaults accessible to the current user.'
322+
),
323+
vals
324+
)

0 commit comments

Comments
 (0)