Completion of control commands entered at the main shell with the ':' prefix
[gsh.git] / gsh / control_shell.py
blobbe24abc42b7ed3ba780ea975c0dd20f125b2ee6a
1 # This program is free software; you can redistribute it and/or modify
2 # it under the terms of the GNU General Public License as published by
3 # the Free Software Foundation; either version 2 of the License, or
4 # (at your option) any later version.
6 # This program is distributed in the hope that it will be useful,
7 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # GNU Library General Public License for more details.
11 # You should have received a copy of the GNU General Public License
12 # along with this program; if not, write to the Free Software
13 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
15 # See the COPYING file for license information.
17 # Copyright (c) 2006, 2007 Guillaume Chazarain <guichaz@yahoo.fr>
19 import asyncore
20 import cmd
21 import os
22 from readline import get_current_history_length, get_history_item
23 from readline import add_history, clear_history
24 import sys
25 import tempfile
26 from fnmatch import fnmatch
28 from gsh.console import console_output
29 from gsh.stdin import the_stdin_thread
30 from gsh.host_syntax import expand_syntax
31 from gsh import dispatchers, remote_dispatcher
33 # The controlling shell, accessible with Ctrl-C
34 singleton = None
36 def make_singleton(options):
37 """Prepate the control shell at initialization time"""
38 global singleton
39 singleton = control_shell(options)
41 def launch():
42 """Ctrl-C was pressed"""
43 return singleton.launch()
45 def toggle_shells(command, enable):
46 """Enable or disable the specified shells"""
47 for i in selected_shells(command):
48 if i.active:
49 i.set_enabled(enable)
51 def selected_shells(command):
52 """Iterator over the shells with names matching the patterns.
53 An empty patterns matches all the shells"""
54 selected = set()
55 for pattern in (command or '*').split():
56 found = False
57 for expanded_pattern in expand_syntax(pattern):
58 for i in dispatchers.all_instances():
59 if fnmatch(i.display_name, expanded_pattern):
60 found = True
61 if i not in selected:
62 selected.add(i)
63 yield i
64 if not found:
65 print pattern, 'not found'
67 def complete_shells(text, line, predicate=lambda i: True):
68 """Return the shell names to include in the completion"""
69 res = [i.display_name + ' ' for i in dispatchers.all_instances() if \
70 i.display_name.startswith(text) and \
71 predicate(i) and \
72 ' ' + i.display_name + ' ' not in line]
73 return res
76 # This file descriptor is used to interrupt readline in raw_input().
77 # /dev/null is not enough as it does not get out of a 'Ctrl-R' reverse-i-search.
78 # A Ctrl-C seems to make raw_input() return in all cases, and avoids printing
79 # a newline
80 tempfile_fd, tempfile_name = tempfile.mkstemp()
81 os.remove(tempfile_name)
82 os.write(tempfile_fd, chr(3))
84 def interrupt_stdin_thread():
85 """The stdin thread may be in raw_input(), get out of it"""
86 if the_stdin_thread.ready_event.isSet():
87 dupped_stdin = os.dup(0) # Backup the stdin fd
88 assert not the_stdin_thread.wants_control_shell
89 the_stdin_thread.wants_control_shell = True # Not user triggered
90 os.lseek(tempfile_fd, 0, 0) # Rewind in the temp file
91 os.dup2(tempfile_fd, 0) # This will make raw_input() return
92 the_stdin_thread.interrupted_event.wait() # Wait for this return
93 the_stdin_thread.wants_control_shell = False
94 os.dup2(dupped_stdin, 0) # Restore stdin
95 os.close(dupped_stdin) # Cleanup
97 def switch_readline_history(new_histo):
98 """Alternate between the command line history from the remote shells (gsh)
99 and the control shell"""
100 xhisto_idx = xrange(1, get_current_history_length() + 1)
101 prev_histo = map(get_history_item, xhisto_idx)
102 clear_history()
103 for line in new_histo:
104 add_history(line)
105 return prev_histo
107 def complete_control_command(text, state):
108 return singleton.modified_cmd_complete(text, state)
110 def handle_control_command(line):
111 singleton.onecmd(line)
113 class control_shell(cmd.Cmd):
114 """The little command line brought when a SIGINT is received"""
115 def __init__(self, options):
116 cmd.Cmd.__init__(self)
117 self.options = options
118 self.prompt = '[ctrl]> '
119 self.history = []
121 def launch(self):
122 if not self.options.interactive:
123 # A Ctrl-C was issued in a non-interactive gsh => exit
124 raise asyncore.ExitNow(1)
125 interrupt_stdin_thread()
126 gsh_histo = switch_readline_history(self.history)
127 console_output('')
128 try:
129 while True:
130 try:
131 cmd.Cmd.cmdloop(self)
132 except KeyboardInterrupt:
133 console_output('\n')
134 else:
135 break
136 finally:
137 self.history = switch_readline_history(gsh_histo)
138 console_output('\r')
140 def completenames(self, text, *ignored):
141 """Overriden to add the trailing space"""
142 return [c + ' ' for c in cmd.Cmd.completenames(self, text, ignored)]
144 def modified_cmd_complete(self, text, state):
145 """Modified to ignore the leading ':'"""
146 if state == 0:
147 import readline
148 origline = readline.get_line_buffer()
149 line = origline.lstrip(':')
150 stripped = len(origline) - len(line)
151 begidx = readline.get_begidx() - stripped
152 endidx = readline.get_endidx() - stripped
153 if begidx>0:
154 cmd, args, foo = self.parseline(line)
155 if cmd == '':
156 compfunc = self.completedefault
157 else:
158 try:
159 compfunc = getattr(self, 'complete_' + cmd)
160 except AttributeError:
161 compfunc = self.completedefault
162 else:
163 compfunc = self.completenames
164 self.completion_matches = compfunc(text, line, begidx, endidx)
165 try:
166 return self.completion_matches[state]
167 except IndexError:
168 return None
170 # We do this just to have 'help' in the 'Documented commands'
171 def do_help(self, command):
173 Usage: help [COMMAND]
174 List available commands or show the documentation of a specific command.
176 return cmd.Cmd.do_help(self, command)
178 def complete_list(self, text, line, begidx, endidx):
179 return complete_shells(text, line)
181 def do_list(self, command):
183 Usage: list [SHELLS...]
184 List the specified or all remote shells and their states.
185 The special characters * ? and [] work as expected.
187 nr_active = nr_dead = 0
188 instances = []
189 for i in selected_shells(command):
190 instances.append(i.get_info())
191 if i.active:
192 nr_active += 1
193 else:
194 nr_dead += 1
195 dispatchers.format_info(instances)
196 print '%s\n\n%d active shells, %d dead shells, total: %d' % \
197 ('\n'.join(instances), nr_active, nr_dead, nr_active + nr_dead)
199 def do_continue(self, command):
201 Usage: continue
202 Go back to gsh.
204 return True
206 def do_EOF(self, command):
208 Usage: Ctrl-D
209 Go back to gsh.
211 return self.do_continue(command)
213 def do_quit(self, command):
215 Usage: quit
216 Quit gsh.
218 raise asyncore.ExitNow(0)
220 def complete_send_control(self, text, line, begidx, endidx):
221 if line[len('send_control'):begidx].strip():
222 # Control letter already given in command line
223 return complete_shells(text, line, lambda i: i.enabled)
224 if text in ('c', 'd', 'z'):
225 return [text + ' ']
226 return ['c', 'd', 'z']
228 def do_send_control(self, command):
230 Usage: send_control LETTER [SHELLS...]
231 Send a control character to the specified or all enabled shells.
232 The first argument is the control character to send: c, d or z.
233 The remaining optional arguments are the destination shells.
234 The special characters * ? and [] work as expected.
236 splitted = command.split()
237 if not splitted:
238 print 'Expected at least a letter'
239 return
240 letter = splitted[0]
241 if len(letter) != 1:
242 print 'Expected a single letter, got:', letter
243 return
244 control_letter = chr(ord(letter.lower()) - ord('a') + 1)
245 for i in selected_shells(' '.join(splitted[1:])):
246 if i.enabled:
247 i.dispatch_write(control_letter)
249 def complete_enable(self, text, line, begidx, endidx):
250 return complete_shells(text, line, lambda i: i.active and not i.enabled)
252 def do_enable(self, command):
254 Usage: enable [SHELLS...]
255 Enable sending commands to all or the specified shells.
256 The special characters * ? and [] work as expected.
258 toggle_shells(command, True)
260 def complete_disable(self, text, line, begidx, endidx):
261 return complete_shells(text, line, lambda i: i.enabled)
263 def do_disable(self, command):
265 Usage: disable [SHELLS...]
266 Disable sending commands to all or the specified shells.
267 The special characters * ? and [] work as expected.
269 toggle_shells(command, False)
271 def complete_reconnect(self, text, line, begidx, endidx):
272 return complete_shells(text, line, lambda i: not i.active)
274 def do_reconnect(self, command):
276 Usage: reconnect [SHELLS...]
277 Try to reconnect to all or the specified remote shells that have been
278 disconnected.
279 The special characters * ? and [] work as expected.
281 for i in selected_shells(command):
282 if not i.active:
283 i.reconnect()
285 def do_add(self, command):
287 Usage: add NAMES...
288 Add one or many remote shells.
290 for host in command.split():
291 remote_dispatcher.remote_dispatcher(self.options, host)
293 def complete_delete_disabled(self, text, line, begidx, endidx):
294 return complete_shells(text, line, lambda i: not i.enabled)
296 def do_delete_disabled(self, command):
298 Usage: delete_disabled [SHELLS...]
299 Delete the specified or all remote processes that are disabled,
300 in order to have a shorter list.
301 The special characters * ? and [] work as expected.
303 to_delete = []
304 for i in selected_shells(command):
305 if not i.enabled:
306 to_delete.append(i)
307 for i in to_delete:
308 i.disconnect()
309 i.close()
311 def do_rename(self, command):
313 Usage: rename [NEW_NAME]
314 Rename all enabled remote processes with the argument. The argument will
315 be shell expanded on the remote processes. With no argument, the
316 original hostname will be restored as the displayed name.
318 for i in dispatchers.all_instances():
319 if i.enabled:
320 i.rename(command)
322 def complete_set_debug(self, text, line, begidx, endidx):
323 if line[len('set_debug'):begidx].strip():
324 # Control letter already given in command line
325 return complete_shells(text, line)
326 if text.lower() in ('y', 'n'):
327 return [text + ' ']
328 return ['y', 'n']
330 def do_set_debug(self, command):
332 Usage: set_debug y|n [SHELLS...]
333 Enable or disable debugging output for all or the specified shells.
334 The first argument is 'y' to enable the debugging output, 'n' to
335 disable it.
336 The remaining optional arguments are the selected shells.
337 The special characters * ? and [] work as expected.
339 splitted = command.split()
340 if not splitted:
341 print 'Expected at least a letter'
342 return
343 letter = splitted[0].lower()
344 if letter not in ('y', 'n'):
345 print "Expected 'y' or 'n', got:", splitted[0]
346 return
347 debug = letter == 'y'
348 for i in selected_shells(' '.join(splitted[1:])):
349 i.debug = debug
351 def emptyline(self):
352 pass