Skip to content

Commit ed7e367

Browse files
author
Juliya Smith
authored
Complete Completion (#88)
1 parent 7b9b232 commit ed7e367

File tree

19 files changed

+396
-92
lines changed

19 files changed

+396
-92
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ how a consumer would use the library (e.g. adding unit tests, updating documenta
8484

8585
- Short option `-u` added for `code42 high-risk-employee add-risk-tags` and `remove-risk-tags`.
8686

87+
- Tab completion for bash and zsh for Unix based machines.
88+
8789
### Fixed
8890

8991
- Fixed bug in bulk commands where value-less fields in csv files were treated as empty strings instead of None.

src/code42cli/cmds/alerts/rules/commands.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ def load_commands(self):
131131
add = Command(
132132
self.ADD_USER,
133133
u"Add a user to an alert rule.",
134-
u"{} add-user --rule-id <id> --username <username>".format(usage_prefix),
134+
u"{} add-user --rule-id <id> --username <username>".format(usage_prefix),
135135
handler=add_user,
136136
arg_customizer=_customize_add_arguments,
137137
)

src/code42cli/cmds/detectionlists/__init__.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -144,16 +144,16 @@ def generate_template_file(self, cmd, path=None):
144144
handler = detection_list.get_handler(self.handlers, cmd)
145145
generate_template(handler, path)
146146

147-
def bulk_add_employees(self, sdk, profile, csv_file):
147+
def bulk_add_employees(self, sdk, profile, filename):
148148
"""Takes a csv file with each row representing an employee and adds them all to a
149149
detection list in a bulk fashion.
150150
151151
Args:
152152
sdk (py42.sdk.SDKClient): The py42 sdk.
153153
profile (Code42Profile): The profile under which to execute this command.
154-
csv_file (str or unicode): The path to the csv file containing rows of users.
154+
filename (str or unicode): The path to the csv file containing rows of users.
155155
"""
156-
reader = create_csv_reader(csv_file)
156+
reader = create_csv_reader(filename)
157157
run_bulk_process(lambda **kwargs: self._add_employee(sdk, profile, **kwargs), reader)
158158

159159
def bulk_remove_employees(self, sdk, profile, users_file):
@@ -176,12 +176,12 @@ def _add_employee(self, sdk, profile, **kwargs):
176176
def _remove_employee(self, sdk, profile, *args, **kwargs):
177177
self.handlers.remove_employee(sdk, profile, *args, **kwargs)
178178

179-
def bulk_add_risk_tags(self, sdk, profile, csv_file):
180-
reader = create_csv_reader(csv_file)
179+
def bulk_add_risk_tags(self, sdk, profile, filename):
180+
reader = create_csv_reader(filename)
181181
run_bulk_process(lambda **kwargs: add_risk_tags(sdk, profile, **kwargs), reader)
182182

183-
def bulk_remove_risk_tags(self, sdk, profile, csv_file):
184-
reader = create_csv_reader(csv_file)
183+
def bulk_remove_risk_tags(self, sdk, profile, filename):
184+
reader = create_csv_reader(filename)
185185
run_bulk_process(lambda **kwargs: remove_risk_tags(sdk, profile, **kwargs), reader)
186186

187187

src/code42cli/cmds/detectionlists/commands.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,8 @@ def _load_hre_bulk_generate_template_description(argument_collection):
146146
cmd_type.set_choices(HighRiskBulkCommandType())
147147

148148
def _load_bulk_add_description(self, argument_collection):
149-
csv_file = argument_collection.arg_configs[u"csv_file"]
150-
csv_file.set_help(
149+
filename = argument_collection.arg_configs[u"filename"]
150+
filename.set_help(
151151
u"The path to the csv file for bulk adding users to the {} detection list.".format(
152152
self._name
153153
)
@@ -162,16 +162,16 @@ def _load_bulk_remove_description(self, argument_collection):
162162
)
163163

164164
def _load_bulk_add_risk_tags_description(self, argument_collection):
165-
csv_file = argument_collection.arg_configs[u"csv_file"]
166-
csv_file.set_help(
165+
filename = argument_collection.arg_configs[u"filename"]
166+
filename.set_help(
167167
u"A file containing a ',' separated username with space-separated tags to add "
168168
u"to the {} detection list. "
169169
u"e.g. [email protected],tag1 tag2 tag3".format(self._name)
170170
)
171171

172172
def _load_bulk_remove_risk_tags_description(self, argument_collection):
173-
csv_file = argument_collection.arg_configs[u"csv_file"]
174-
csv_file.set_help(
173+
filename = argument_collection.arg_configs[u"filename"]
174+
filename.set_help(
175175
u"A file containing a ',' separated username with space-separated tags to remove "
176176
u"from the {} detection list. "
177177
u"e.g. [email protected],tag1 tag2 tag3".format(self._name)

src/code42cli/cmds/legal_hold/__init__.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,16 +59,12 @@ def get_matters(sdk):
5959

6060
def add_bulk_users(sdk, file_name):
6161
reader = create_csv_reader(file_name)
62-
run_bulk_process(
63-
lambda matter_id, username: add_user(sdk, matter_id, username), reader,
64-
)
62+
run_bulk_process(lambda matter_id, username: add_user(sdk, matter_id, username), reader)
6563

6664

6765
def remove_bulk_users(sdk, file_name):
6866
reader = create_csv_reader(file_name)
69-
run_bulk_process(
70-
lambda matter_id, username: remove_user(sdk, matter_id, username), reader,
71-
)
67+
run_bulk_process(lambda matter_id, username: remove_user(sdk, matter_id, username), reader)
7268

7369

7470
def show_matter(sdk, matter_id, include_inactive=False, include_policy=False):

src/code42cli/cmds/securitydata/main.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@ def _load_send_to_args(arg_collection):
100100
help=u"Protocol used to send logs to server.",
101101
),
102102
}
103-
104103
arg_collection.extend(send_to_args)
105104
_load_search_args(arg_collection)
106105

src/code42cli/commands.py

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from code42cli import profile as cliprofile
44
from code42cli.args import get_auto_arg_configs, SDK_ARG_NAME, PROFILE_ARG_NAME
55
from code42cli.sdk_client import create_sdk
6+
from code42cli.tree_nodes import SubcommandNode
67

78

89
class DictObject(object):
@@ -150,29 +151,24 @@ def _kvps_to_obj(kvps):
150151

151152

152153
class SubcommandLoader(object):
153-
"""Responsible for creating subcommands for it's root command. It is also useful for getting
154-
command information ahead of time, as in the example of tab completion."""
154+
"""Responsible for creating subcommands for it's root command."""
155155

156-
def __init__(self, root_command_name):
156+
def __init__(self, root_command_name, node=None):
157157
self.root = root_command_name
158+
self._node = node
158159

159-
@property
160-
def names(self):
161-
"""The names of all the subcommands in this subcommabd loader's root command."""
162-
sub_cmds = self.load_commands()
163-
return [cmd.name for cmd in sub_cmds]
160+
def __getitem__(self, item):
161+
return self.get_node()[item]
164162

165163
@property
166-
def subtrees(self):
167-
"""All subcommands for this subcommand loader's root command mapped to their given
168-
subcommand loaders."""
169-
cmds = self.load_commands()
170-
results = {}
171-
for cmd in cmds:
172-
subcommand_loader = cmd.subcommand_loader
173-
if subcommand_loader:
174-
results[cmd.name] = subcommand_loader
175-
return results
164+
def names(self):
165+
return self.get_node().names
176166

177167
def load_commands(self):
168+
"""Override"""
178169
return []
170+
171+
def get_node(self):
172+
if not self._node:
173+
self._node = SubcommandNode(self.root, self.load_commands())
174+
return self._node

src/code42cli/completer.py

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
from os import path
2+
13
from code42cli import MAIN_COMMAND
24
from code42cli.main import MainSubcommandLoader
5+
from code42cli.tree_nodes import ArgNode
6+
from code42cli.util import get_files_in_path
37

48

59
def _get_matches(current, options):
@@ -11,46 +15,67 @@ def _get_matches(current, options):
1115
return matches
1216

1317

14-
def _get_next_full_set_of_commands(cmd_loader, current):
15-
cmd_loader = cmd_loader.subtrees[current]
16-
return cmd_loader.names
18+
def _get_next_full_set_of_options(node, current):
19+
node = node[current]
20+
names = list(node.names)
21+
if _can_complete_with_local_files(current, node):
22+
files = get_files_in_path("")
23+
names.extend(files)
24+
return names
25+
26+
27+
def _can_complete_with_local_files(current, node):
28+
return isinstance(node, ArgNode) and (not current or current[0] != u"-")
1729

1830

1931
class Completer(object):
2032
def __init__(self, main_cmd_loader=None):
21-
self._main_cmd_loader = main_cmd_loader or MainSubcommandLoader(u"")
33+
self._main_cmd_loader = main_cmd_loader or MainSubcommandLoader()
2234

2335
def complete(self, cmdline, point=None):
2436
try:
2537
point = point or len(cmdline)
2638
args = cmdline[0:point].split()
39+
# Complete with main commands if `code42` is typed out.
40+
# Note that the command `code42` should complete on its own.
2741
if len(args) < 2:
28-
# `code42` already completes w/o
2942
return self._main_cmd_loader.names if args[0] == MAIN_COMMAND else []
3043

3144
current = args[-1]
32-
cmd_loader = self._search_trees(args)
33-
if not cmd_loader:
34-
return []
45+
search_results, options = self._get_completion_options(args)
3546

36-
options = cmd_loader.names
47+
# Complete with full set of arg/command options
3748
if current in options:
38-
# `current` is already complete
39-
return _get_next_full_set_of_commands(cmd_loader, current)
49+
return _get_next_full_set_of_options(search_results, current)
50+
51+
if _can_complete_with_local_files(current, search_results):
52+
files = get_files_in_path(current)
53+
if current[0] == "~":
54+
replace = path.expanduser("~")
55+
files = [f.replace(replace, "~") for f in files]
56+
options.extend(files)
4057

4158
return _get_matches(current, options) if options else []
4259
except:
4360
return []
4461

4562
def _search_trees(self, args):
4663
# Find cmd_loader at lowest level from given args
47-
cmd_loader = self._main_cmd_loader
64+
node = self._main_cmd_loader.get_node()
4865
if len(args) > 2:
4966
for arg in args[1:-1]:
50-
cmd_loader = cmd_loader.subtrees[arg]
51-
return cmd_loader
67+
next_node = node[arg]
68+
if next_node:
69+
node = next_node
70+
else:
71+
return node
72+
return node
73+
74+
def _get_completion_options(self, args):
75+
search_results = self._search_trees(args)
76+
return search_results, search_results.names
5277

5378

5479
def complete(cmdline, point):
55-
choices = Completer().complete(cmdline, point)
80+
choices = Completer().complete(cmdline, point) or []
5681
print(u" \n".join(choices))

src/code42cli/main.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ class MainSubcommandLoader(SubcommandLoader):
5353
HIGH_RISK_EMPLOYEE = DetectionLists.HIGH_RISK_EMPLOYEE
5454
LEGAL_HOLD = u"legal-hold"
5555

56+
def __init__(self):
57+
super(MainSubcommandLoader, self).__init__(u"")
58+
5659
def load_commands(self):
5760
detection_lists_description = (
5861
u"For adding and removing employees from the {} detection list."
@@ -118,7 +121,7 @@ def _create_legal_hold_loader(self):
118121

119122

120123
def main():
121-
top = Command(u"", u"", subcommand_loader=MainSubcommandLoader(u""))
124+
top = Command(u"", u"", subcommand_loader=MainSubcommandLoader())
122125
invoker = CommandInvoker(top)
123126
try:
124127
invoker.run(sys.argv[1:])

src/code42cli/tree_nodes.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
class CLINode(object):
2+
"""Base class for identifying nodes in the command/argument hierarchy."""
3+
4+
@property
5+
def names(self):
6+
"""Override"""
7+
return []
8+
9+
10+
class ChoicesNode(CLINode):
11+
"""A node who `names` refer to choices the user can select for an argument."""
12+
13+
def __init__(self, options):
14+
self._choices = options
15+
16+
def __iter__(self):
17+
return iter(self._choices)
18+
19+
def __getitem__(self, item):
20+
return self._choices[item]
21+
22+
def get(self, item):
23+
return self._choices.get(item)
24+
25+
@property
26+
def names(self):
27+
return self._choices
28+
29+
30+
class ArgNode(CLINode):
31+
"""A node whose `names` are a list of flagged arguments the user can select from."""
32+
33+
def __init__(self, args):
34+
self.args = args
35+
36+
@property
37+
def names(self):
38+
try:
39+
arg_names = [
40+
n
41+
for names in [self.args[key].settings[u"options_list"] for key in self.args]
42+
for n in names
43+
if n.startswith("--")
44+
]
45+
return arg_names
46+
except:
47+
return self.args
48+
49+
def __getitem__(self, item):
50+
"""Access sub loaders to navigate the argument/options tree, connected to a leaf command."""
51+
if item in self.args:
52+
return ArgNode(self.args)
53+
54+
for key in self.args:
55+
arg = self.args[key]
56+
if item not in arg.settings[u"options_list"]:
57+
continue
58+
choices = arg.settings[u"choices"]
59+
if choices:
60+
return ChoicesNode(choices)
61+
return ArgNode(self.args)
62+
63+
def __iter__(self):
64+
return iter(self.names)
65+
66+
67+
class SubcommandNode(CLINode):
68+
"""Gets command information ahead of command-execution."""
69+
70+
def __init__(self, root_command_name, commands):
71+
self.root = root_command_name
72+
self.commands = commands
73+
74+
def __getitem__(self, item):
75+
try:
76+
return self._subtrees[item]
77+
except KeyError:
78+
return self._get_args(item)
79+
80+
def _get_args(self, item):
81+
cmd = self._get_command_by_name(item)
82+
if cmd:
83+
args = cmd.get_arg_configs()
84+
return ArgNode(args)
85+
86+
def _get_command_by_name(self, name):
87+
for cmd in self.commands:
88+
if cmd.name == name:
89+
return cmd
90+
91+
@property
92+
def names(self):
93+
"""The names of all the subcommands in this subcommand loader's root command."""
94+
return [cmd.name for cmd in self.commands]
95+
96+
@property
97+
def _subtrees(self):
98+
"""Maps subcommand names to their respective subcommand nodes."""
99+
results = {}
100+
for cmd in self.commands:
101+
if cmd.subcommand_loader:
102+
commands = cmd.subcommand_loader.load_commands()
103+
results[cmd.name] = SubcommandNode(cmd.name, commands)
104+
return results

0 commit comments

Comments
 (0)