blob: e90d21a3669ea2d523e3af6d402ebe4b22dac82f [file] [log] [blame]
#!/usr/bin/env python
# Copyright 2011 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Command-line client for tr/rcommand.py."""
__author__ = 'apenwarr@google.com (Avery Pennarun)'
import os.path
import re
import socket
import sys
import traceback
import google3
import bup.options
import bup.shquote
import tr.mainloop
import tr.quotedblock
try:
import readline # pylint:disable=g-import-not-at-top
except ImportError:
readline = None
optspec = """
cwmp [options] [command...]
--
u,unix-path= Unix socket server is listening on [/tmp/cwmpd.sock]
i,ip= IP hostname/ipaddr server is listening on (default: unix socket)
p,port= IP port server is listening on [12999]
"""
HISTORY_FILE = os.path.expanduser('/tmp/.cwmp_history')
def Log(s, *args):
s = str(s)
if args:
sys.stderr.write((s + '\n') % args)
else:
sys.stderr.write(s + '\n')
_want_verbose = False
def Verbose(s, *args):
if _want_verbose:
Log(s, *args)
class Fatal(Exception):
pass
def HandleFatal(func):
def Fn(*args, **kwargs):
try:
return func(*args, **kwargs)
except Fatal, e:
Log('Fatal: %s' % e)
sys.exit(1)
return Fn
def _NormalizePath(path):
"""Like os.path.normpath, but doesn't remove any trailing slash."""
result = os.path.normpath(path)
if path.endswith('/') and not result.endswith('/'):
result += '/'
return result
def _DotsToSlashes(s):
return re.sub(r'([^/.])\.', r'\1/', s)
def _SlashesToDots(s):
name = s.replace('/', '.')
if name.startswith('.'):
name = name[1:]
return name
class Client(object):
"""Manage the client-side state of an rcommand connection."""
def __init__(self, loop, connector):
self.loop = loop
self.connector = connector
self.stream = None
self.result = None
self._last_res = (None, None, None)
self.cwd = '/'
self.quotedblock = tr.quotedblock.QuotedBlockProtocol(
HandleFatal(self.GotBlock))
self._StartConnect()
def _StartConnect(self):
self.stream = None
try:
self.connector(HandleFatal(self.OnConnect))
except socket.error, e:
raise Fatal(str(e))
def Close(self):
if self.stream:
self.stream.close()
def OnConnect(self, stream):
if not stream:
raise Fatal('connection failed')
Verbose('Connected to server.\n')
self.stream = stream
self.stream.set_close_callback(HandleFatal(self.OnClose))
self._StartRead()
self.loop.ioloop.stop()
def _StartRead(self):
self.stream.read_until('\r\n\r\n', HandleFatal(self.GotData))
def OnClose(self):
Log('Server connection closed!')
self._StartConnect()
def GotData(self, data):
assert data.endswith('\r\n\r\n')
for i in data.split('\r\n')[:-1]:
self.quotedblock.GotData(i)
self._StartRead()
def GotBlock(self, lines):
self.result = lines
self.loop.ioloop.stop()
def Send(self, lines):
s = self.quotedblock.RenderBlock(lines)
self.stream.write(s)
def Run(self, lines):
self.Send(lines)
self.loop.Start()
result = self.result
self.result = None
return result
def _RequestCompletions(self, prefix):
prefix = _NormalizePath(prefix)
completions = self.Run([['completions', _SlashesToDots(prefix)]])[1:]
for [i] in completions:
yield i
def _GetSubstitutions(self, line):
(qtype, lastword) = bup.shquote.unfinished_word(line)
request = os.path.join(self.cwd, _DotsToSlashes(lastword))
subs = list(self._RequestCompletions(request))
cmd = line.split(' ', 1)[0]
if cmd.lower() in ('cd', 'ls', 'list', 'rlist', 'validate', 'add', 'del'):
# only return object names, not parameters
subs = [i for i in subs if i.endswith('.')]
return (qtype, lastword, subs)
def _StripPathPrefix(self, oldword, newword):
# readline is weird: we have to remove all the parts that were before
# the last '/', but not parts before the last '.', because we have to
# tell it what to replace everything after the last '/' with.
after_slash = oldword.split('/')[-1]
dots = after_slash.split('.')
if newword.endswith('.'):
new_last_dot = '.'.join(newword.split('.')[-2:])
else:
new_last_dot = newword.split('.')[-1]
dots[-1] = new_last_dot
return '.'.join(dots)
def ReadlineCompleter(self, text, state):
"""Callback for the readline library to autocomplete a line of text.
Args:
text: the current input word (basename following the last slash)
state: a number of 0..n, where n is the number of substitutions.
Returns:
One of the available substitutions.
"""
try:
text = _DotsToSlashes(text)
line = readline.get_line_buffer()[:readline.get_endidx()]
if not state:
self._last_res = self._GetSubstitutions(line)
(qtype, lastword, subs) = self._last_res
if state < len(subs):
new_last_slash = _DotsToSlashes(self._StripPathPrefix(lastword,
subs[state]))
is_param = not new_last_slash.endswith('/')
if is_param and qtype:
new_last_slash += qtype
return new_last_slash
except Exception, e: # pylint:disable=broad-except
Log('\n')
try:
traceback.print_tb(sys.exc_traceback)
except Exception, e2: # pylint:disable=broad-except
Log('Error printing traceback: %s\n' % e2)
Log('\nError in completion: %s\n' % e)
def DoCmd(client, words):
"""Send the command to cwmpd."""
words = [i.decode('utf-8') for i in words]
cmd, args = (words[0].lower(), words[1:])
if cmd in ('cd', 'ls', 'list', 'rlist', 'validate',
'add', 'del', 'get', 'set'):
if not args:
args = [client.cwd]
skip = 2 if cmd in ('add', 'del', 'set') else 1
for i in range(0, len(args), skip):
relpath = _DotsToSlashes(args[i])
abspath = os.path.normpath(os.path.join(client.cwd, relpath))
args[i] = _SlashesToDots(abspath)
if cmd == 'cd':
client.cwd = os.path.normpath(os.path.join(client.cwd, relpath))
else:
line = [cmd] + args
result = client.Run([line])
return result
def Interactive(client):
"""Run an interactive command prompt on the given connection."""
global _want_verbose
_want_verbose = True
if readline:
readline.set_completer_delims(' \t\n\r/')
readline.set_completer(client.ReadlineCompleter)
# this. drove. me. INSANE.
# http://stackoverflow.com/questions/7124035
if 'libedit' in readline.__doc__:
readline.parse_and_bind('bind ^I rl_complete') # MacOS
else:
readline.parse_and_bind('tab: complete') # other
while True:
print
line = raw_input('%s> ' % client.cwd) + '\n'
while 1:
word = bup.shquote.unfinished_word(line)[1]
if not word:
break
line += raw_input('%*s> ' % (len(client.cwd), '')) + '\n'
words = [word for (unused_idx, word) in bup.shquote.quotesplit(line)]
if not words:
continue
result = DoCmd(client, words)
print client.quotedblock.RenderBlock(result).strip()
def main():
o = bup.options.Options(optspec)
(opt, unused_flags, extra) = o.parse(sys.argv[1:])
if readline and os.path.exists(HISTORY_FILE):
readline.read_history_file(HISTORY_FILE)
client = None
try:
loop = tr.mainloop.MainLoop()
if opt.ip is not None:
connector = lambda err: loop.ConnectInet((opt.ip, opt.port), err)
else:
connector = lambda err: loop.ConnectUnix(opt.unix_path, err)
client = Client(loop, connector)
loop.Start()
if extra:
result = DoCmd(client, extra)
code = result.pop(0)
if code[0] != 'OK':
raise Fatal(' '.join(code))
for line in result:
print ' '.join(line)
else:
Interactive(client)
except Fatal, e:
Log(e)
sys.exit(1)
except EOFError:
pass
finally:
if readline:
readline.write_history_file(HISTORY_FILE)
if client:
client.Close()
if __name__ == '__main__':
main()