Contact
CoCalc Logo Icon
StoreFeaturesDocsShareSupport News AboutSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/scripts/shell_completions.py
Views: 275
1
#!/usr/bin/env python
2
###############################################################################
3
#
4
# CoCalc: Collaborative Calculation
5
#
6
# Copyright (C) 2016, Sagemath Inc.
7
#
8
# This program is free software: you can redistribute it and/or modify
9
# it under the terms of the GNU General Public License as published by
10
# the Free Software Foundation, either version 3 of the License, or
11
# (at your option) any later version.
12
#
13
# This program is distributed in the hope that it will be useful,
14
# but WITHOUT ANY WARRANTY; without even the implied warranty of
15
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
# GNU General Public License for more details.
17
#
18
# You should have received a copy of the GNU General Public License
19
# along with this program. If not, see <http://www.gnu.org/licenses/>.
20
#
21
###############################################################################
22
23
# Known bugs/oddities:
24
# [ ] Fails to correctly detect the prompt with certain .bashrc settings.
25
# [X] Spurious empty strings/escape sequences appear with multi-page output.
26
# [ ] Output is not consistent. Example: 'git stat' returns 'git status', while
27
# 'git sta' returns a list of completions only for the 'sta' part of the
28
# command.
29
30
import argparse, pexpect, sys
31
32
COMPLETIONS_COMMAND = ". /etc/bash_completion"
33
BIGLIST_WARNING = "(y or n)"
34
NEXT_PAGE_INDICATOR = "--More--"
35
DEFAULT_TIMEOUT = 2
36
DEFAULT_SHELL = "bash"
37
38
39
def print_string(message, string):
40
print message + " \"" + string + "\""
41
42
43
def completions(partial_command,
44
shell=DEFAULT_SHELL,
45
return_raw=False,
46
import_completions=True,
47
get_prompt=True,
48
prompt="$ ",
49
timeout=DEFAULT_TIMEOUT,
50
biglist=True,
51
verbose=False,
52
logfile=None):
53
"""
54
Returns a list containing the tab completions found by the shell for the
55
input string.
56
"""
57
58
child = pexpect.spawn(shell, timeout=timeout)
59
60
if verbose:
61
child.logfile = sys.stdout
62
63
if logfile is not None:
64
logfile = open(logfile, "w")
65
child.logfile = logfile
66
67
# We want to echo characters sent to the shell so that new prompts print
68
# on their own line.
69
child.setecho(True)
70
71
# echo_on = child.getecho()
72
# if verbose:
73
# print "Echo state: " + str(echo_on)
74
75
# Get a bare command prompt in order to find the end of the
76
# list of completions.
77
if get_prompt:
78
79
# !!!
80
# Here we assume that the shell will only print out a command
81
# prompt on startup. This is not always true.
82
# !!!
83
child.sendline()
84
child.expect_exact("\r\n")
85
prompt = child.before
86
87
# We just hit enter, so we expect the shell to print a new prompt and
88
# we need to clear it out of the buffer.
89
child.expect_exact(prompt)
90
91
if verbose:
92
print_string("Prompt:", prompt)
93
94
# Run a script to configure extra bash completions.
95
if import_completions:
96
child.sendline(COMPLETIONS_COMMAND)
97
child.expect_exact(prompt)
98
99
child.send(partial_command + "\t\t")
100
child.expect_exact(partial_command)
101
#### NOTE: I don't understand why this time we don't get an echo.
102
#### New idea: Of course... it's only echoing the sent characters.
103
# child.expect_exact(partial_command)
104
105
index = child.expect_exact([" ", "\r\n", pexpect.TIMEOUT])
106
107
if index == 0:
108
# Bash found a single completion and filled it in.
109
return [partial_command + child.before]
110
111
elif index == 1:
112
index = child.expect_exact(
113
[BIGLIST_WARNING, NEXT_PAGE_INDICATOR, prompt])
114
if index == 0 or index == 1:
115
# The shell found too many completions to list on one screen.
116
if biglist:
117
completions = ""
118
119
# For very long lists the shell asks whether to continue.
120
if index == 0:
121
child.send("y")
122
123
# Shorter lists print to the screen without asking.
124
else:
125
completions += child.before
126
child.send(" ")
127
128
# Keep sending space to get more pages until we get back to the
129
# command prompt.
130
while True:
131
index = child.expect_exact([NEXT_PAGE_INDICATOR, prompt])
132
completions += child.before
133
if index == 0:
134
child.send(" ")
135
elif index == 1:
136
break
137
138
# Remove spurious escape sequence.
139
completions = completions.replace("\x1b[K", "")
140
141
elif index == 2:
142
# Bash found more than one completion and listed them on multiple lines.
143
# child.expect_exact(prompt)
144
completions = child.before
145
146
elif index == 2:
147
# If the command timed out, either no completion was found or it
148
# found a single completion witout adding a space (for instance, this
149
# happens when completing the name of an executable).
150
151
# print_string("Timed out:", child.before)
152
153
# Remove any bell characters the shell appended to the completion.
154
return [partial_command + child.buffer.replace("\x07", "")]
155
156
child.close()
157
158
# Parse the completions into a Python list of strings.
159
return completions.split()
160
161
162
if __name__ == "__main__":
163
164
parser = argparse.ArgumentParser(
165
description=
166
"Returns the tab completions found by the shell for the input string.")
167
168
parser.add_argument(
169
"COMMAND",
170
type=str,
171
help="The partial command that the shell should attempt to complete.")
172
173
parser.add_argument(
174
"--no_biglists",
175
action="store_false",
176
default=True,
177
help="Abort execution if the shell finds a large number of completions."
178
)
179
180
parser.add_argument(
181
"--no_detect_prompt",
182
action="store_false",
183
default=True,
184
help=
185
"Don't attempt to detect the command prompt, and use a built-in constant instead. This should speed up execution times."
186
)
187
188
parser.add_argument(
189
"--no_import_completions",
190
default=True,
191
help=
192
"Don't set up completions by running the script at /etc/completions.")
193
parser.add_argument(
194
"--raw",
195
action="store_false",
196
default=False,
197
help="Returns all output from the shell without formatting changes.")
198
199
parser.add_argument(
200
"--separator",
201
"-s",
202
default="\n",
203
help="Character used to separate the list of completions.")
204
205
parser.add_argument(
206
"--shell",
207
default="bash",
208
help="The shell to query for completions. Defaults to bash.")
209
210
parser.add_argument(
211
"--timeout",
212
"-t",
213
metavar="SECONDS",
214
type=float,
215
default=DEFAULT_TIMEOUT,
216
help="The time in seconds before the program detects no shell output.")
217
218
parser.add_argument(
219
"--verbose",
220
"-v",
221
action="store_true",
222
default=False,
223
help="Verbose mode.")
224
225
parser.add_argument(
226
"--log",
227
"-l",
228
metavar="LOGFILE",
229
default=None,
230
help="Log all shell output to file named LOGFILE.")
231
232
args = parser.parse_args()
233
234
completion_list = completions(
235
args.COMMAND,
236
verbose=args.verbose,
237
return_raw=args.raw,
238
get_prompt=args.no_detect_prompt,
239
timeout=args.timeout,
240
biglist=args.no_biglists,
241
logfile=args.log)
242
243
print str(args.separator).join(completion_list)
244
245