Strip some control commands parameters, as we don't take
[gsh.git] / gsh / control_commands.py
blobfb01630714a86f0e1416ab089d575c1e8022cc35
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, 2008 Guillaume Chazarain <guichaz@gmail.com>
19 import asyncore
20 import os
21 import shutil
22 import sys
23 import tempfile
25 from gsh.control_commands_helpers import complete_shells, selected_shells
26 from gsh.control_commands_helpers import list_control_commands
27 from gsh.control_commands_helpers import get_control_command, toggle_shells
28 from gsh.control_commands_helpers import expand_local_path
29 from gsh.completion import complete_local_path
30 from gsh.console import console_output
31 from gsh.version import VERSION
32 from gsh import dispatchers
33 from gsh import remote_dispatcher
34 from gsh import stdin
35 from gsh import file_transfer
37 def complete_help(line, text):
38 colon = text.startswith(':')
39 text = text.lstrip(':')
40 res = [cmd + ' ' for cmd in list_control_commands() if \
41 cmd.startswith(text) and ' ' + cmd + ' ' not in line]
42 if colon:
43 res = [':' + cmd for cmd in res]
44 return res
46 def do_help(command):
47 """
48 Usage: :help [COMMAND]
49 List control commands or show their documentations.
50 """
51 command = command.strip()
52 if command:
53 texts = []
54 for name in command.split():
55 try:
56 cmd = get_control_command(name.lstrip(':'))
57 except AttributeError:
58 console_output('Unknown control command: %s\n' % name)
59 else:
60 doc = [d.strip() for d in cmd.__doc__.split('\n') if d.strip()]
61 texts.append('\n'.join(doc))
62 if texts:
63 console_output('\n\n'.join(texts))
64 console_output('\n')
65 else:
66 names = list_control_commands()
67 max_name_len = max(map(len, names))
68 for i in xrange(len(names)):
69 name = names[i]
70 txt = ':' + name + (max_name_len - len(name) + 2) * ' '
71 doc = get_control_command(name).__doc__
72 txt += doc.split('\n')[2].strip() + '\n'
73 console_output(txt)
75 def complete_list(line, text):
76 return complete_shells(line, text)
78 def do_list(command):
79 """
80 Usage: :list [SHELLS...]
81 List remote shells and their states.
82 The output consists of: <hostname> <enabled?> <state>: <last printed line>.
83 The special characters * ? and [] work as expected.
84 """
85 instances = [i.get_info() for i in selected_shells(command)]
86 dispatchers.format_info(instances)
87 console_output(''.join(instances))
89 def do_quit(command):
90 """
91 Usage: :quit
92 Quit gsh.
93 """
94 raise asyncore.ExitNow(0)
96 def complete_chdir(line, text):
97 return filter(os.path.isdir, complete_local_path(text))
99 def do_chdir(command):
101 Usage: :chdir LOCAL_PATH
102 Change the current directory of gsh (not the remote shells).
104 try:
105 os.chdir(expand_local_path(command.strip()))
106 except OSError, e:
107 console_output('%s\n' % str(e))
109 def complete_send_ctrl(line, text):
110 if len(line[:-1].split()) >= 2:
111 # Control letter already given in command line
112 return complete_shells(line, text, lambda i: i.enabled)
113 if text in ('c', 'd', 'z'):
114 return [text + ' ']
115 return ['c ', 'd ', 'z ']
117 def do_send_ctrl(command):
119 Usage: :send_ctrl LETTER [SHELLS...]
120 Send a control character to remote shells.
121 The first argument is the control character to send like c, d or z.
122 Note that these three control characters can be sent simply by typing them
123 into gsh.
124 The remaining optional arguments are the destination shells.
125 The special characters * ? and [] work as expected.
127 split = command.split()
128 if not split:
129 console_output('Expected at least a letter\n')
130 return
131 letter = split[0]
132 if len(letter) != 1:
133 console_output('Expected a single letter, got: %s\n' % letter)
134 return
135 control_letter = chr(ord(letter.lower()) - ord('a') + 1)
136 for i in selected_shells(' '.join(split[1:])):
137 if i.enabled:
138 i.dispatch_write(control_letter)
140 def complete_reset_prompt(line, text):
141 return complete_shells(line, text, lambda i: i.enabled)
143 def do_reset_prompt(command):
145 Usage: :reset_prompt [SHELLS...]
146 Change the prompt to be recognized by gsh.
147 The special characters * ? and [] work as expected.
149 for i in selected_shells(command):
150 i.dispatch_command(i.init_string)
152 def complete_enable(line, text):
153 return complete_shells(line, text, lambda i:
154 i.state != remote_dispatcher.STATE_DEAD and not i.enabled)
156 def do_enable(command):
158 Usage: :enable [SHELLS...]
159 Enable sending commands to remote shells.
160 If the command would have no effect, it changes all other shells to the
161 inverse enable value. That is, if you enable only already enabled
162 shells, it will first disable all other shells.
163 The special characters * ? and [] work as expected.
165 toggle_shells(command, True)
167 def complete_disable(line, text):
168 return complete_shells(line, text, lambda i: i.enabled)
170 def do_disable(command):
172 Usage: :disable [SHELLS...]
173 Disable sending commands to remote shells.
174 If the command would have no effect, it changes all other shells to the
175 inverse enable value. That is, if you disable only already disabled
176 shells, it will first enable all other shells.
177 The special characters * ? and [] work as expected.
179 toggle_shells(command, False)
181 def complete_reconnect(line, text):
182 return complete_shells(line, text, lambda i:
183 i.state == remote_dispatcher.STATE_DEAD)
185 def do_reconnect(command):
187 Usage: :reconnect [SHELLS...]
188 Try to reconnect to disconnected remote shells.
189 The special characters * ? and [] work as expected.
191 for i in selected_shells(command):
192 if i.state == remote_dispatcher.STATE_DEAD:
193 i.reconnect()
195 def do_add(command):
197 Usage: :add NAMES...
198 Add one or many remote shells.
200 for host in command.split():
201 remote_dispatcher.remote_dispatcher(host)
203 def complete_purge(line, text):
204 return complete_shells(line, text, lambda i: not i.enabled)
206 def do_purge(command):
208 Usage: :purge [SHELLS...]
209 Delete disabled remote shells.
210 This helps to have a shorter list.
211 The special characters * ? and [] work as expected.
213 to_delete = []
214 for i in selected_shells(command):
215 if not i.enabled:
216 to_delete.append(i)
217 for i in to_delete:
218 i.disconnect()
219 i.close()
221 def do_rename(command):
223 Usage: :rename [NEW_NAME]
224 Rename all enabled remote shells with the argument.
225 The argument will be shell expanded on the remote processes. With no
226 argument, the original hostname will be restored as the displayed name.
228 for i in dispatchers.all_instances():
229 if i.enabled:
230 i.rename(command)
232 def do_hide_password(command):
234 Usage: :hide_password
235 Do not echo the next typed line.
236 This is useful when entering password. If debugging or logging is enabled,
237 it will be disabled to avoid displaying a password. Therefore, you will have
238 to reenable logging or debugging afterwards if need be.
240 warned = False
241 for i in dispatchers.all_instances():
242 if i.enabled and i.debug:
243 i.debug = False
244 if not warned:
245 console_output('Debugging disabled to avoid displaying '
246 'passwords\n')
247 warned = True
248 stdin.set_echo(False)
250 if remote_dispatcher.options.log_file:
251 console_output('Logging disabled to avoid writing passwords\n')
252 remote_dispatcher.options.log_file = None
254 def complete_set_debug(line, text):
255 if len(line[:-1].split()) >= 2:
256 # Debug value already given in command line
257 return complete_shells(line, text)
258 if text.lower() in ('y', 'n'):
259 return [text + ' ']
260 return ['y ', 'n ']
262 def do_set_debug(command):
264 Usage: :set_debug y|n [SHELLS...]
265 Enable or disable debugging output for remote shells.
266 The first argument is 'y' to enable the debugging output, 'n' to
267 disable it.
268 The remaining optional arguments are the selected shells.
269 The special characters * ? and [] work as expected.
271 split = command.split()
272 if not split:
273 console_output('Expected at least a letter\n')
274 return
275 letter = split[0].lower()
276 if letter not in ('y', 'n'):
277 console_output("Expected 'y' or 'n', got: %s\n" % split[0])
278 return
279 debug = letter == 'y'
280 for i in selected_shells(' '.join(split[1:])):
281 i.debug = debug
283 def complete_replicate(line, text):
284 if ':' not in text:
285 enabled_shells = complete_shells(line, text, lambda i: i.enabled)
286 return [c[:-1] + ':' for c in enabled_shells]
287 shell, path = text.split(':')
288 return [shell + ':' + p for p in complete_local_path(path)]
290 def do_replicate(command):
292 Usage: :replicate SHELL:REMOTE_PATH
293 Copy a path from one remote shell to all others
295 if ':' not in command:
296 console_output('Usage: :replicate SHELL:REMOTE_PATH\n')
297 return
298 shell_name, path = command.strip().split(':', 1)
299 if not path:
300 console_output('No remote path given\n')
301 return
302 for shell in dispatchers.all_instances():
303 if shell.display_name == shell_name:
304 if not shell.enabled:
305 console_output('%s is not enabled\n' % shell_name)
306 return
307 break
308 else:
309 console_output('%s not found\n' % shell_name)
310 return
311 file_transfer.replicate(shell, path)
313 def complete_upload(line, text):
314 return complete_local_path(text)
316 def do_upload(command):
318 Usage: :upload LOCAL_PATH
319 Upload the specified local path to enabled remote shells.
321 if command:
322 file_transfer.upload(command.strip())
323 else:
324 console_output('No local path given\n')
326 def do_export_rank(command):
328 Usage: :export_rank
329 Set GSH_RANK and GSH_NR_SHELLS on enabled remote shells.
330 The GSH_RANK shell variable uniquely identifies each shell with a number
331 between 0 and GSH_NR_SHELLS - 1. GSH_NR_SHELLS is the total number of
332 enabled shells.
334 rank = 0
335 for shell in dispatchers.all_instances():
336 if shell.enabled:
337 shell.dispatch_command('export GSH_RANK=%d\n' % rank)
338 rank += 1
340 for shell in dispatchers.all_instances():
341 if shell.enabled:
342 shell.dispatch_command('export GSH_NR_SHELLS=%d\n' % rank)
344 def complete_set_log(line, text):
345 return complete_local_path(text)
347 def do_set_log(command):
349 Usage: :set_log [LOCAL_PATH]
350 Duplicate every console I/O into the given local file.
351 If LOCAL_PATH is not given, restore the default behaviour of not logging.
353 command = command.strip()
354 if command:
355 try:
356 remote_dispatcher.options.log_file = file(command, 'a')
357 except IOError, e:
358 console_output('%s\n' % str(e))
359 command = None
360 if not command:
361 remote_dispatcher.options.log_file = None
362 console_output('Logging disabled\n')
364 def complete_show_read_buffer(line, text):
365 return complete_shells(line, text, lambda i: i.read_buffer or
366 i.read_in_state_not_started)
368 def do_show_read_buffer(command):
370 Usage: :show_read_buffer [SHELLS...]
371 Print the data read by remote shells.
372 The special characters * ? and [] work as expected.
374 for i in selected_shells(command):
375 if i.read_in_state_not_started:
376 i.print_lines(i.read_in_state_not_started)
377 i.read_in_state_not_started = ''
379 def main():
381 Output a help text of each control command suitable for the man page
382 Run from the gsh top directory: python -m gsh.control_commands
384 try:
385 man_page = file('gsh.1', 'r')
386 except IOError, e:
387 print e
388 print 'Please run "python -m gsh.control_commands" from the gsh top' + \
389 ' directory'
390 sys.exit(1)
392 updated_man_page_fd, updated_man_page_path = tempfile.mkstemp()
393 updated_man_page = os.fdopen(updated_man_page_fd, 'w')
395 # The first line is auto-generated as it contains the version number
396 man_page.readline()
397 v = '.TH "gsh" "1" "%s" "Guillaume Chazarain" "Remote shells"' % VERSION
398 print >> updated_man_page, v
400 for line in man_page:
401 print >> updated_man_page, line,
402 if 'BEGIN AUTO-GENERATED CONTROL COMMANDS DOCUMENTATION' in line:
403 break
405 for name in list_control_commands():
406 print >> updated_man_page, '.TP'
407 unstripped = get_control_command(name).__doc__.split('\n')
408 lines = [l.strip() for l in unstripped]
409 usage = lines[1].strip()
410 print >> updated_man_page, '\\fB%s\\fR' % usage[7:]
411 help_text = ' '.join(lines[2:]).replace('gsh', '\\fIgsh\\fR').strip()
412 print >> updated_man_page, help_text
414 for line in man_page:
415 if 'END AUTO-GENERATED CONTROL COMMANDS DOCUMENTATION' in line:
416 print >> updated_man_page, line,
417 break
419 for line in man_page:
420 print >> updated_man_page, line,
422 man_page.close()
423 updated_man_page.close()
424 shutil.move(updated_man_page_path, 'gsh.1')
426 if __name__ == '__main__':
427 main()