More scalable display_name handling (actually the length).
[gsh.git] / gsh / control_commands.py
blob4d1e9614feb17cea9e67d6c5e54aef81178d4d3e
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)
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:
169 i.state != remote_dispatcher.STATE_DEAD)
171 def do_disable(command):
173 Usage: :disable [SHELLS...]
174 Disable sending commands to remote shells.
175 If the command would have no effect, it changes all other shells to the
176 inverse enable value. That is, if you disable only already disabled
177 shells, it will first enable all other shells.
178 The special characters * ? and [] work as expected.
180 toggle_shells(command, False)
182 def complete_reconnect(line, text):
183 return complete_shells(line, text, lambda i:
184 i.state == remote_dispatcher.STATE_DEAD)
186 def do_reconnect(command):
188 Usage: :reconnect [SHELLS...]
189 Try to reconnect to disconnected remote shells.
190 The special characters * ? and [] work as expected.
192 for i in selected_shells(command):
193 if i.state == remote_dispatcher.STATE_DEAD:
194 i.reconnect()
196 def do_add(command):
198 Usage: :add NAMES...
199 Add one or many remote shells.
201 for host in command.split():
202 remote_dispatcher.remote_dispatcher(host)
204 def complete_purge(line, text):
205 return complete_shells(line, text, lambda i: not i.enabled)
207 def do_purge(command):
209 Usage: :purge [SHELLS...]
210 Delete disabled remote shells.
211 This helps to have a shorter list.
212 The special characters * ? and [] work as expected.
214 to_delete = []
215 for i in selected_shells(command):
216 if not i.enabled:
217 to_delete.append(i)
218 for i in to_delete:
219 i.disconnect()
220 i.close()
222 def do_rename(command):
224 Usage: :rename [NEW_NAME]
225 Rename all enabled remote shells with the argument.
226 The argument will be shell expanded on the remote processes. With no
227 argument, the original hostname will be restored as the displayed name.
229 for i in dispatchers.all_instances():
230 if i.enabled:
231 i.rename(command)
233 def do_hide_password(command):
235 Usage: :hide_password
236 Do not echo the next typed line.
237 This is useful when entering password. If debugging or logging is enabled,
238 it will be disabled to avoid displaying a password. Therefore, you will have
239 to reenable logging or debugging afterwards if need be.
241 warned = False
242 for i in dispatchers.all_instances():
243 if i.enabled and i.debug:
244 i.debug = False
245 if not warned:
246 console_output('Debugging disabled to avoid displaying '
247 'passwords\n')
248 warned = True
249 stdin.set_echo(False)
251 if remote_dispatcher.options.log_file:
252 console_output('Logging disabled to avoid writing passwords\n')
253 remote_dispatcher.options.log_file = None
255 def complete_set_debug(line, text):
256 if len(line[:-1].split()) >= 2:
257 # Debug value already given in command line
258 return complete_shells(line, text)
259 if text.lower() in ('y', 'n'):
260 return [text + ' ']
261 return ['y ', 'n ']
263 def do_set_debug(command):
265 Usage: :set_debug y|n [SHELLS...]
266 Enable or disable debugging output for remote shells.
267 The first argument is 'y' to enable the debugging output, 'n' to
268 disable it.
269 The remaining optional arguments are the selected shells.
270 The special characters * ? and [] work as expected.
272 split = command.split()
273 if not split:
274 console_output('Expected at least a letter\n')
275 return
276 letter = split[0].lower()
277 if letter not in ('y', 'n'):
278 console_output("Expected 'y' or 'n', got: %s\n" % split[0])
279 return
280 debug = letter == 'y'
281 for i in selected_shells(' '.join(split[1:])):
282 i.debug = debug
284 def complete_replicate(line, text):
285 if ':' not in text:
286 enabled_shells = complete_shells(line, text, lambda i: i.enabled)
287 return [c[:-1] + ':' for c in enabled_shells]
288 shell, path = text.split(':')
289 return [shell + ':' + p for p in complete_local_path(path)]
291 def do_replicate(command):
293 Usage: :replicate SHELL:REMOTE_PATH
294 Copy a path from one remote shell to all others
296 if ':' not in command:
297 console_output('Usage: :replicate SHELL:REMOTE_PATH\n')
298 return
299 shell_name, path = command.strip().split(':', 1)
300 if not path:
301 console_output('No remote path given\n')
302 return
303 for shell in dispatchers.all_instances():
304 if shell.display_name == shell_name:
305 if not shell.enabled:
306 console_output('%s is not enabled\n' % shell_name)
307 return
308 break
309 else:
310 console_output('%s not found\n' % shell_name)
311 return
312 file_transfer.replicate(shell, path)
314 def complete_upload(line, text):
315 return complete_local_path(text)
317 def do_upload(command):
319 Usage: :upload LOCAL_PATH
320 Upload the specified local path to enabled remote shells.
322 if command:
323 file_transfer.upload(command.strip())
324 else:
325 console_output('No local path given\n')
327 def do_export_rank(command):
329 Usage: :export_rank
330 Set GSH_RANK and GSH_NR_SHELLS on enabled remote shells.
331 The GSH_RANK shell variable uniquely identifies each shell with a number
332 between 0 and GSH_NR_SHELLS - 1. GSH_NR_SHELLS is the total number of
333 enabled shells.
335 rank = 0
336 for shell in dispatchers.all_instances():
337 if shell.enabled:
338 shell.dispatch_command('export GSH_RANK=%d\n' % rank)
339 rank += 1
341 for shell in dispatchers.all_instances():
342 if shell.enabled:
343 shell.dispatch_command('export GSH_NR_SHELLS=%d\n' % rank)
345 def complete_set_log(line, text):
346 return complete_local_path(text)
348 def do_set_log(command):
350 Usage: :set_log [LOCAL_PATH]
351 Duplicate every console I/O into the given local file.
352 If LOCAL_PATH is not given, restore the default behaviour of not logging.
354 command = command.strip()
355 if command:
356 try:
357 remote_dispatcher.options.log_file = file(command, 'a')
358 except IOError, e:
359 console_output('%s\n' % str(e))
360 command = None
361 if not command:
362 remote_dispatcher.options.log_file = None
363 console_output('Logging disabled\n')
365 def complete_show_read_buffer(line, text):
366 return complete_shells(line, text, lambda i: i.read_buffer or
367 i.read_in_state_not_started)
369 def do_show_read_buffer(command):
371 Usage: :show_read_buffer [SHELLS...]
372 Print the data read by remote shells.
373 The special characters * ? and [] work as expected.
375 for i in selected_shells(command):
376 if i.read_in_state_not_started:
377 i.print_lines(i.read_in_state_not_started)
378 i.read_in_state_not_started = ''
380 def main():
382 Output a help text of each control command suitable for the man page
383 Run from the gsh top directory: python -m gsh.control_commands
385 try:
386 man_page = file('gsh.1', 'r')
387 except IOError, e:
388 print e
389 print 'Please run "python -m gsh.control_commands" from the gsh top' + \
390 ' directory'
391 sys.exit(1)
393 updated_man_page_fd, updated_man_page_path = tempfile.mkstemp()
394 updated_man_page = os.fdopen(updated_man_page_fd, 'w')
396 # The first line is auto-generated as it contains the version number
397 man_page.readline()
398 v = '.TH "gsh" "1" "%s" "Guillaume Chazarain" "Remote shells"' % VERSION
399 print >> updated_man_page, v
401 for line in man_page:
402 print >> updated_man_page, line,
403 if 'BEGIN AUTO-GENERATED CONTROL COMMANDS DOCUMENTATION' in line:
404 break
406 for name in list_control_commands():
407 print >> updated_man_page, '.TP'
408 unstripped = get_control_command(name).__doc__.split('\n')
409 lines = [l.strip() for l in unstripped]
410 usage = lines[1].strip()
411 print >> updated_man_page, '\\fB%s\\fR' % usage[7:]
412 help_text = ' '.join(lines[2:]).replace('gsh', '\\fIgsh\\fR').strip()
413 print >> updated_man_page, help_text
415 for line in man_page:
416 if 'END AUTO-GENERATED CONTROL COMMANDS DOCUMENTATION' in line:
417 print >> updated_man_page, line,
418 break
420 for line in man_page:
421 print >> updated_man_page, line,
423 man_page.close()
424 updated_man_page.close()
425 shutil.move(updated_man_page_path, 'gsh.1')
427 if __name__ == '__main__':
428 main()