Rename gsh to polysh.
[polysh.git] / polysh / control_commands.py
blob2778e9d22979ec9898375f8beaeb17910a57ed72
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 Guillaume Chazarain <guichaz@gmail.com>
19 import asyncore
20 import os
21 import pipes
22 import shutil
23 import sys
24 import tempfile
26 from polysh.control_commands_helpers import complete_shells, selected_shells
27 from polysh.control_commands_helpers import list_control_commands
28 from polysh.control_commands_helpers import get_control_command, toggle_shells
29 from polysh.control_commands_helpers import expand_local_path
30 from polysh.completion import complete_local_path, add_to_history
31 from polysh.console import console_output
32 from polysh.version import VERSION
33 from polysh import dispatchers
34 from polysh import remote_dispatcher
35 from polysh import stdin
36 from polysh import file_transfer
38 def complete_help(line, text):
39 colon = text.startswith(':')
40 text = text.lstrip(':')
41 res = [cmd + ' ' for cmd in list_control_commands() if \
42 cmd.startswith(text) and ' ' + cmd + ' ' not in line]
43 if colon:
44 res = [':' + cmd for cmd in res]
45 return res
47 def do_help(command):
48 """
49 Usage: :help [COMMAND]
50 List control commands or show their documentations.
51 """
52 command = command.strip()
53 if command:
54 texts = []
55 for name in command.split():
56 try:
57 cmd = get_control_command(name.lstrip(':'))
58 except AttributeError:
59 console_output('Unknown control command: %s\n' % name)
60 else:
61 doc = [d.strip() for d in cmd.__doc__.split('\n') if d.strip()]
62 texts.append('\n'.join(doc))
63 if texts:
64 console_output('\n\n'.join(texts))
65 console_output('\n')
66 else:
67 names = list_control_commands()
68 max_name_len = max(map(len, names))
69 for i in xrange(len(names)):
70 name = names[i]
71 txt = ':' + name + (max_name_len - len(name) + 2) * ' '
72 doc = get_control_command(name).__doc__
73 txt += doc.split('\n')[2].strip() + '\n'
74 console_output(txt)
76 def complete_list(line, text):
77 return complete_shells(line, text)
79 def do_list(command):
80 """
81 Usage: :list [SHELLS...]
82 List remote shells and their states.
83 The output consists of: <hostname> <enabled?> <state>: <last printed line>.
84 The special characters * ? and [] work as expected.
85 """
86 instances = [i.get_info() for i in selected_shells(command)]
87 dispatchers.format_info(instances)
88 console_output(''.join(instances))
90 def do_quit(command):
91 """
92 Usage: :quit
93 Quit polysh.
94 """
95 raise asyncore.ExitNow(0)
97 def complete_chdir(line, text):
98 return filter(os.path.isdir, complete_local_path(text))
100 def do_chdir(command):
102 Usage: :chdir LOCAL_PATH
103 Change the current directory of polysh (not the remote shells).
105 try:
106 os.chdir(expand_local_path(command.strip()))
107 except OSError, e:
108 console_output('%s\n' % str(e))
110 def complete_send_ctrl(line, text):
111 if len(line[:-1].split()) >= 2:
112 # Control letter already given in command line
113 return complete_shells(line, text, lambda i: i.enabled)
114 if text in ('c', 'd', 'z'):
115 return [text + ' ']
116 return ['c ', 'd ', 'z ']
118 def do_send_ctrl(command):
120 Usage: :send_ctrl LETTER [SHELLS...]
121 Send a control character to remote shells.
122 The first argument is the control character to send like c, d or z.
123 Note that these three control characters can be sent simply by typing them
124 into polysh.
125 The remaining optional arguments are the destination shells.
126 The special characters * ? and [] work as expected.
128 split = command.split()
129 if not split:
130 console_output('Expected at least a letter\n')
131 return
132 letter = split[0]
133 if len(letter) != 1:
134 console_output('Expected a single letter, got: %s\n' % letter)
135 return
136 control_letter = chr(ord(letter.lower()) - ord('a') + 1)
137 for i in selected_shells(' '.join(split[1:])):
138 if i.enabled:
139 i.dispatch_write(control_letter)
141 def complete_reset_prompt(line, text):
142 return complete_shells(line, text, lambda i: i.enabled)
144 def do_reset_prompt(command):
146 Usage: :reset_prompt [SHELLS...]
147 Change the prompt to be recognized by polysh.
148 The special characters * ? and [] work as expected.
150 for i in selected_shells(command):
151 i.dispatch_command(i.init_string)
153 def complete_enable(line, text):
154 return complete_shells(line, text, lambda i:
155 i.state != remote_dispatcher.STATE_DEAD)
157 def do_enable(command):
159 Usage: :enable [SHELLS...]
160 Enable sending commands to remote shells.
161 If the command would have no effect, it changes all other shells to the
162 inverse enable value. That is, if you enable only already enabled
163 shells, it will first disable all other shells.
164 The special characters * ? and [] work as expected.
166 toggle_shells(command, True)
168 def complete_disable(line, text):
169 return complete_shells(line, text, lambda i:
170 i.state != remote_dispatcher.STATE_DEAD)
172 def do_disable(command):
174 Usage: :disable [SHELLS...]
175 Disable sending commands to remote shells.
176 If the command would have no effect, it changes all other shells to the
177 inverse enable value. That is, if you disable only already disabled
178 shells, it will first enable all other shells.
179 The special characters * ? and [] work as expected.
181 toggle_shells(command, False)
183 def complete_reconnect(line, text):
184 return complete_shells(line, text, lambda i:
185 i.state == remote_dispatcher.STATE_DEAD)
187 def do_reconnect(command):
189 Usage: :reconnect [SHELLS...]
190 Try to reconnect to disconnected remote shells.
191 The special characters * ? and [] work as expected.
193 selec = selected_shells(command)
194 to_reconnect = [i for i in selec if i.state == remote_dispatcher.STATE_DEAD]
195 for i in to_reconnect:
196 i.disconnect()
197 i.close()
199 hosts = [i.hostname for i in to_reconnect]
200 dispatchers.create_remote_dispatchers(hosts)
202 def do_add(command):
204 Usage: :add NAMES...
205 Add one or many remote shells.
207 dispatchers.create_remote_dispatchers(command.split())
209 def complete_purge(line, text):
210 return complete_shells(line, text, lambda i: not i.enabled)
212 def do_purge(command):
214 Usage: :purge [SHELLS...]
215 Delete disabled remote shells.
216 This helps to have a shorter list.
217 The special characters * ? and [] work as expected.
219 to_delete = []
220 for i in selected_shells(command):
221 if not i.enabled:
222 to_delete.append(i)
223 for i in to_delete:
224 i.disconnect()
225 i.close()
227 def do_rename(command):
229 Usage: :rename [NEW_NAME]
230 Rename all enabled remote shells with the argument.
231 The argument will be shell expanded on the remote processes. With no
232 argument, the original hostname will be restored as the displayed name.
234 for i in dispatchers.all_instances():
235 if i.enabled:
236 i.rename(command)
238 def do_hide_password(command):
240 Usage: :hide_password
241 Do not echo the next typed line.
242 This is useful when entering password. If debugging or logging is enabled,
243 it will be disabled to avoid displaying a password. Therefore, you will have
244 to reenable logging or debugging afterwards if need be.
246 warned = False
247 for i in dispatchers.all_instances():
248 if i.enabled and i.debug:
249 i.debug = False
250 if not warned:
251 console_output('Debugging disabled to avoid displaying '
252 'passwords\n')
253 warned = True
254 stdin.set_echo(False)
256 if remote_dispatcher.options.log_file:
257 console_output('Logging disabled to avoid writing passwords\n')
258 remote_dispatcher.options.log_file = None
260 def complete_set_debug(line, text):
261 if len(line[:-1].split()) >= 2:
262 # Debug value already given in command line
263 return complete_shells(line, text)
264 if text.lower() in ('y', 'n'):
265 return [text + ' ']
266 return ['y ', 'n ']
268 def do_set_debug(command):
270 Usage: :set_debug y|n [SHELLS...]
271 Enable or disable debugging output for remote shells.
272 The first argument is 'y' to enable the debugging output, 'n' to
273 disable it.
274 The remaining optional arguments are the selected shells.
275 The special characters * ? and [] work as expected.
277 split = command.split()
278 if not split:
279 console_output('Expected at least a letter\n')
280 return
281 letter = split[0].lower()
282 if letter not in ('y', 'n'):
283 console_output("Expected 'y' or 'n', got: %s\n" % split[0])
284 return
285 debug = letter == 'y'
286 for i in selected_shells(' '.join(split[1:])):
287 i.debug = debug
289 def complete_replicate(line, text):
290 if ':' not in text:
291 enabled_shells = complete_shells(line, text, lambda i: i.enabled)
292 return [c[:-1] + ':' for c in enabled_shells]
293 shell, path = text.split(':')
294 return [shell + ':' + p for p in complete_local_path(path)]
296 def do_replicate(command):
298 Usage: :replicate SHELL:REMOTE_PATH
299 Copy a path from one remote shell to all others
301 if ':' not in command:
302 console_output('Usage: :replicate SHELL:REMOTE_PATH\n')
303 return
304 shell_name, path = command.strip().split(':', 1)
305 if not path:
306 console_output('No remote path given\n')
307 return
308 for shell in dispatchers.all_instances():
309 if shell.display_name == shell_name:
310 if not shell.enabled:
311 console_output('%s is not enabled\n' % shell_name)
312 return
313 break
314 else:
315 console_output('%s not found\n' % shell_name)
316 return
317 file_transfer.replicate(shell, path)
319 def complete_upload(line, text):
320 return complete_local_path(text)
322 def do_upload(command):
324 Usage: :upload LOCAL_PATH
325 Upload the specified local path to enabled remote shells.
327 if command:
328 file_transfer.upload(command.strip())
329 else:
330 console_output('No local path given\n')
332 def do_export_vars(command):
334 Usage: :export_vars
335 Export some environment variables on enabled remote shells.
336 POLYSH_NR_SHELLS is the total number of enabled shells. POLYSH_RANK uniquely
337 identifies each shell with a number between 0 and POLYSH_NR_SHELLS - 1.
338 POLYSH_NAME is the hostname as specified on the command line and
339 POLYSH_DISPLAY_NAME the hostname as displayed by :list (most of the time the
340 same as POLYSH_NAME).
342 rank = 0
343 for shell in dispatchers.all_instances():
344 if shell.enabled:
345 environment_variables = {
346 'POLYSH_RANK': rank,
347 'POLYSH_NAME': shell.hostname,
348 'POLYSH_DISPLAY_NAME': shell.display_name,
350 for name, value in environment_variables.iteritems():
351 value = pipes.quote(str(value))
352 shell.dispatch_command('export %s=%s\n' % (name, value))
353 rank += 1
355 for shell in dispatchers.all_instances():
356 if shell.enabled:
357 shell.dispatch_command('export POLYSH_NR_SHELLS=%d\n' % rank)
359 add_to_history('$POLYSH_RANK $POLYSH_NAME $POLYSH_DISPLAY_NAME')
360 add_to_history('$POLYSH_NR_SHELLS')
362 def complete_set_log(line, text):
363 return complete_local_path(text)
365 def do_set_log(command):
367 Usage: :set_log [LOCAL_PATH]
368 Duplicate every console I/O into the given local file.
369 If LOCAL_PATH is not given, restore the default behaviour of not logging.
371 command = command.strip()
372 if command:
373 try:
374 remote_dispatcher.options.log_file = file(command, 'a')
375 except IOError, e:
376 console_output('%s\n' % str(e))
377 command = None
378 if not command:
379 remote_dispatcher.options.log_file = None
380 console_output('Logging disabled\n')
382 def complete_show_read_buffer(line, text):
383 return complete_shells(line, text, lambda i: i.read_buffer or
384 i.read_in_state_not_started)
386 def do_show_read_buffer(command):
388 Usage: :show_read_buffer [SHELLS...]
389 Print the data read by remote shells.
390 The special characters * ? and [] work as expected.
392 for i in selected_shells(command):
393 if i.read_in_state_not_started:
394 i.print_lines(i.read_in_state_not_started)
395 i.read_in_state_not_started = ''
397 def main():
399 Output a help text of each control command suitable for the man page
400 Run from the polysh top directory: python -m polysh.control_commands
402 try:
403 man_page = file('polysh.1', 'r')
404 except IOError, e:
405 print e
406 print 'Please run "python -m polysh.control_commands" from the' + \
407 ' polysh top directory'
408 sys.exit(1)
410 updated_man_page_fd, updated_man_page_path = tempfile.mkstemp()
411 updated_man_page = os.fdopen(updated_man_page_fd, 'w')
413 # The first line is auto-generated as it contains the version number
414 man_page.readline()
415 v = '.TH "polysh" "1" "%s" "Guillaume Chazarain" "Remote shells"' % VERSION
416 print >> updated_man_page, v
418 for line in man_page:
419 print >> updated_man_page, line,
420 if 'BEGIN AUTO-GENERATED CONTROL COMMANDS DOCUMENTATION' in line:
421 break
423 for name in list_control_commands():
424 print >> updated_man_page, '.TP'
425 unstripped = get_control_command(name).__doc__.split('\n')
426 lines = [l.strip() for l in unstripped]
427 usage = lines[1].strip()
428 print >> updated_man_page, '\\fB%s\\fR' % usage[7:]
429 help_text = ' '.join(lines[2:]).replace('polysh', '\\fIpolysh\\fR')
430 print >> updated_man_page, help_text.strip()
432 for line in man_page:
433 if 'END AUTO-GENERATED CONTROL COMMANDS DOCUMENTATION' in line:
434 print >> updated_man_page, line,
435 break
437 for line in man_page:
438 print >> updated_man_page, line,
440 man_page.close()
441 updated_man_page.close()
442 shutil.move(updated_man_page_path, 'polysh.1')
444 if __name__ == '__main__':
445 main()