Skip to content

Commit c45e406

Browse files
authored
Merge pull request #450 from mbartling/bash-completion
Added tab completion for mbed cli on bash
2 parents ae86dc3 + 08febbf commit c45e406

File tree

14 files changed

+1129
-0
lines changed

14 files changed

+1129
-0
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,12 @@ To uninstall mbed CLI, run:
104104
pip uninstall mbed-cli
105105
```
106106

107+
### Adding Bash tab completion
108+
109+
To install mbed-cli bash tab completion navigate to the `tools/bash_completion` directory. Then copy the `mbed` script into your `/etc/bash_completion.d/` or `/usr/local/etc/bash_completion.d` directory and reload your terminal.
110+
111+
[Full documentation here](tools/bash_completion/install.md)
112+
107113
## Quickstart video
108114

109115
<span class="images">[![Video tutorial](http://img.youtube.com/vi/PI1Kq9RSN_Y/0.jpg)](https://www.youtube.com/watch?v=PI1Kq9RSN_Y)</span>

tools/bash_completion/generator.py

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
#!/usr/bin/env python
2+
# Michael Bartling ([email protected])
3+
4+
from collections import defaultdict
5+
import pystache
6+
import re
7+
import subprocess
8+
9+
# Top level --version is a pain to deal with so ignoring for now
10+
# This one extracts single commands and the help txt
11+
commandRegex = r"^\s+(?P<command>\w+)\s+(?P<helptxt>[a-zA-Z ]*)$"
12+
13+
# Why the hell do spaces get regexed in command1 ?
14+
subcommandRegex = r"^\s+(?P<command1>-+[a-zA-Z_\-]+(?P<modifier1>\s+[A-Z_\-]+)?)"\
15+
r"(?P<command2>,\s+-+[a-zA-Z_-]+(?P<modifier2>\s+[A-Z_-]+)?)?"\
16+
r"\s+(?P<helptxt>.*)$"
17+
18+
19+
def getHelpTxt(command=None):
20+
if command:
21+
p = subprocess.Popen(["mbed", command, "-h"], stdout=subprocess.PIPE)
22+
else:
23+
p = subprocess.Popen(["mbed", "-h"], stdout=subprocess.PIPE)
24+
out, err = p.communicate()
25+
return out
26+
27+
def getTargetCode():
28+
txt = ''
29+
with open("templates/target.tmplt") as fp:
30+
txt = fp.read()
31+
return txt
32+
33+
def getToolchainCode():
34+
txt = ''
35+
with open("templates/toolchain.tmplt") as fp:
36+
txt = fp.read()
37+
return txt
38+
39+
def getSCMCode():
40+
txt = ''
41+
with open("templates/scm.tmplt") as fp:
42+
txt = fp.read()
43+
return txt
44+
45+
def getIDECode():
46+
txt = ''
47+
with open("templates/ide.tmplt") as fp:
48+
txt = fp.read()
49+
return txt
50+
51+
def getProtocolCode():
52+
txt = ''
53+
with open("templates/protocol.tmplt") as fp:
54+
txt = fp.read()
55+
return txt
56+
57+
def parseCommands():
58+
commands = defaultdict(defaultdict)
59+
commands["COMMAND"] = []
60+
helpTxt = getHelpTxt()
61+
# print helpTxt
62+
for line in helpTxt.split('\n'):
63+
match = re.search(commandRegex, line)
64+
if match:
65+
g = match.groupdict()
66+
commands[g["command"]]["helptxt"] = g["helptxt"]
67+
commands[g["command"]]["subcommands"] = []
68+
69+
# Subcommand mustache generation
70+
commands[g["command"]]["DDASH_COMMANDS"] = []
71+
commands[g["command"]]["DASH_COMMANDS"] = []
72+
commands[g["command"]]["COMMAND"] = g["command"]
73+
74+
commands[g["command"]]["HAVE_PREV"] = {"PREV_CASE": []}
75+
76+
# Main function generation
77+
commands["COMMAND"].append({"name": g["command"]})
78+
79+
for commandKey in commands:
80+
# Skip
81+
if commandKey == "COMMAND":
82+
continue
83+
84+
helpTxt = getHelpTxt(commandKey)
85+
for line in helpTxt.split('\n'):
86+
match = re.search(subcommandRegex, line)
87+
if match:
88+
commandMatch = match.groupdict()
89+
90+
# Clean up the subcommands
91+
command1 = commandMatch["command1"]
92+
command2 = commandMatch["command2"]
93+
94+
if command1:
95+
command1 = re.sub(",", "", command1)
96+
command1.strip()
97+
command1 = command1.split()[0]
98+
if command2:
99+
command2 = re.sub(",", "", command2)
100+
command2.strip()
101+
command2 = command2.split()[0]
102+
103+
# Not sure why the cleaning is even necessary,
104+
# the regex looks correct
105+
commandMatch["command1"] = command1
106+
commandMatch["command2"] = command2
107+
108+
commands[commandKey]["subcommands"].append(commandMatch)
109+
110+
# Push format for mustache
111+
if command1 and '--' in command1:
112+
commands[commandKey]["DDASH_COMMANDS"].append(
113+
{"name": command1})
114+
if command2 and '--' in command2:
115+
commands[commandKey]["DDASH_COMMANDS"].append(
116+
{"name": command2})
117+
118+
if command1:
119+
m = re.match("^-[a-zA-Z]{1,2}", command1)
120+
if m:
121+
commands[commandKey]["DASH_COMMANDS"].append(
122+
{"name": command1})
123+
else:
124+
command1 = ""
125+
126+
if command2:
127+
m = re.match("^-[a-zA-Z]{1,2}", command2)
128+
if m:
129+
commands[commandKey]["DASH_COMMANDS"].append(
130+
{"name": command2})
131+
else:
132+
command2 = ""
133+
134+
# Adding the dependent command handlers
135+
if "target" in command1 or "target" in command2:
136+
commands[commandKey]["HAVE_PREV"]["PREV_CASE"].append({"case": "|".join(filter(None, [command1, command2])), "code": getTargetCode()})
137+
138+
if "toolchain" in command1 or "toolchain" in command2:
139+
commands[commandKey]["HAVE_PREV"]["PREV_CASE"].append({"case": "|".join(filter(None, [command1, command2])), "code": getToolchainCode()})
140+
141+
142+
if "--ide" in command1 or "--ide" in command2:
143+
commands[commandKey]["HAVE_PREV"]["PREV_CASE"].append({"case": "|".join(filter(None, [command1, command2])), "code": getIDECode()})
144+
145+
if "scm" in command1 or "scm" in command2:
146+
commands[commandKey]["HAVE_PREV"]["PREV_CASE"].append({"case": "|".join(filter(None, [command1, command2])), "code": getSCMCode()})
147+
148+
if "protocol" in command1 or "protocol" in command2:
149+
commands[commandKey]["HAVE_PREV"]["PREV_CASE"].append({"case": "|".join(filter(None, [command1, command2])), "code": getProtocolCode()})
150+
151+
# Adding the dependent command handlers for target and toolchain
152+
if "target" in commandKey:
153+
commands[commandKey]["HAVE_PREV"]["PREV_CASE"].append({"case": commandKey, "code": getTargetCode()})
154+
155+
if "toolchain" in commandKey:
156+
commands[commandKey]["HAVE_PREV"]["PREV_CASE"].append({"case": commandKey, "code": getToolchainCode()})
157+
158+
return commands
159+
160+
161+
def generateMain(commands):
162+
tmplt = ""
163+
164+
txt = []
165+
166+
with open("templates/mbed.tmplt") as fp:
167+
tmplt = fp.read()
168+
169+
txt.append(pystache.render(tmplt, commands))
170+
171+
return txt
172+
173+
174+
def generateCompleters(commands):
175+
tmplt = ""
176+
txt = []
177+
178+
renderer = pystache.Renderer(escape=lambda u: u)
179+
180+
with open("templates/command.tmplt") as fp:
181+
tmplt = fp.read()
182+
183+
for commandKey in commands:
184+
txt.append(renderer.render(tmplt, commands[commandKey]))
185+
186+
# if need to add hacks add them here
187+
188+
return txt
189+
190+
191+
def generateBoilerPlate(commands):
192+
txt = []
193+
194+
with open("templates/boilerplate.tmplt") as fp:
195+
txt.append(fp.read())
196+
197+
return txt
198+
199+
200+
def generateScript(commands):
201+
txt = []
202+
203+
txt.extend(generateBoilerPlate(commands))
204+
txt.extend(generateCompleters(commands))
205+
txt.extend(generateMain(commands))
206+
207+
with open("mbed-completion", "w") as fp:
208+
for x in txt:
209+
fp.write("%s\n" % x)
210+
211+
212+
if __name__ == '__main__':
213+
commands = parseCommands()
214+
215+
# At this point we have a list of all the commands and sub commands
216+
# for each command create a Bash function
217+
# register each subcommand
218+
generateScript(commands)

tools/bash_completion/install.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Install Guide
2+
3+
## System-wide Installation (root)
4+
5+
- Copy or link the bash completion script, `mbed`, into your `/etc/bash_completion.d` or `/usr/local/etc/bash_completion.d` directory.
6+
- Reopen terminal
7+
8+
## Local Installation
9+
10+
- `mkdir ~/.bash_completion.d && cp mbed ~/.bash_completion.d/`
11+
- `echo "source ~/.bash_completion.d/mbed" >> ~/.bash_profile`
12+
- logout and login
13+

0 commit comments

Comments
 (0)