[tor-commits] [stem/master] Fixing interpretor misspelling

atagar at torproject.org atagar at torproject.org
Sat May 24 20:52:15 UTC 2014


commit c5a7bbe46555f9a922639942561786c7155ca4f0
Author: Damian Johnson <atagar at torproject.org>
Date:   Thu May 22 09:15:51 2014 -0700

    Fixing interpretor misspelling
    
    Huh. Not sure why my spell checker thought 'interpretor' was ok. Thanks to
    Yawning for pointing this out.
---
 docs/change_log.rst                   |    2 +-
 setup.py                              |    2 +-
 stem/interpreter/__init__.py          |  129 ++++++++++++++
 stem/interpreter/arguments.py         |   96 +++++++++++
 stem/interpreter/autocomplete.py      |  112 ++++++++++++
 stem/interpreter/commands.py          |  299 +++++++++++++++++++++++++++++++++
 stem/interpreter/help.py              |  142 ++++++++++++++++
 stem/interpreter/settings.cfg         |  295 ++++++++++++++++++++++++++++++++
 stem/interpretor/__init__.py          |  129 --------------
 stem/interpretor/arguments.py         |   96 -----------
 stem/interpretor/autocomplete.py      |  112 ------------
 stem/interpretor/commands.py          |  299 ---------------------------------
 stem/interpretor/help.py              |  142 ----------------
 stem/interpretor/settings.cfg         |  295 --------------------------------
 stem/util/system.py                   |    2 +-
 test/settings.cfg                     |    8 +-
 test/unit/interpreter/__init__.py     |   39 +++++
 test/unit/interpreter/arguments.py    |   57 +++++++
 test/unit/interpreter/autocomplete.py |  112 ++++++++++++
 test/unit/interpreter/commands.py     |  198 ++++++++++++++++++++++
 test/unit/interpreter/help.py         |   54 ++++++
 test/unit/interpretor/__init__.py     |   39 -----
 test/unit/interpretor/arguments.py    |   57 -------
 test/unit/interpretor/autocomplete.py |  112 ------------
 test/unit/interpretor/commands.py     |  198 ----------------------
 test/unit/interpretor/help.py         |   54 ------
 tor-prompt                            |    4 +-
 27 files changed, 1542 insertions(+), 1542 deletions(-)

diff --git a/docs/change_log.rst b/docs/change_log.rst
index f89de76..ef9005d 100644
--- a/docs/change_log.rst
+++ b/docs/change_log.rst
@@ -66,7 +66,7 @@ The following are only available within Stem's `git repository
 
  * **Interpretor**
 
-  * Initial release of a Tor interactive interpretor. This included...
+  * Initial release of a Tor interactive interpreter. This included...
 
    * irc-style functions such as '/help' and '/info'
    * history scroll-back by pressing up/down
diff --git a/setup.py b/setup.py
index 47eb478..45071e5 100644
--- a/setup.py
+++ b/setup.py
@@ -49,7 +49,7 @@ setup(
   author = module_info['author'],
   author_email = module_info['contact'],
   url = module_info['url'],
-  packages = ['stem', 'stem.descriptor', 'stem.interpretor', 'stem.response', 'stem.util'],
+  packages = ['stem', 'stem.descriptor', 'stem.interpreter', 'stem.response', 'stem.util'],
   provides = ['stem'],
   cmdclass = {'build_py': build_py},
   keywords = 'tor onion controller',
diff --git a/stem/interpreter/__init__.py b/stem/interpreter/__init__.py
new file mode 100644
index 0000000..23453a3
--- /dev/null
+++ b/stem/interpreter/__init__.py
@@ -0,0 +1,129 @@
+# Copyright 2014, Damian Johnson and The Tor Project
+# See LICENSE for licensing information
+
+"""
+Interactive interpreter for interacting with Tor directly. This adds usability
+features such as tab completion, history, and IRC-style functions (like /help).
+"""
+
+__all__ = ['arguments', 'autocomplete', 'commands', 'help', 'msg']
+
+import os
+import sys
+
+import stem
+import stem.connection
+import stem.process
+import stem.util.conf
+import stem.util.system
+import stem.util.term
+
+from stem.util.term import RESET, Attr, Color, format
+
+# Our color prompt triggers a bug between raw_input() and readline history,
+# where scrolling through history widens our prompt. Widening our prompt via
+# invisible characters (like resets) seems to sidestep this bug for short
+# inputs. Contrary to the ticket, this still manifests with python 2.7.1...
+#
+#   http://bugs.python.org/issue12972
+
+PROMPT = format('>>> ', Color.GREEN, Attr.BOLD) + RESET * 10
+
+STANDARD_OUTPUT = (Color.BLUE, )
+BOLD_OUTPUT = (Color.BLUE, Attr.BOLD)
+HEADER_OUTPUT = (Color.GREEN, )
+HEADER_BOLD_OUTPUT = (Color.GREEN, Attr.BOLD)
+ERROR_OUTPUT = (Attr.BOLD, Color.RED)
+
+settings_path = os.path.join(os.path.dirname(__file__), 'settings.cfg')
+uses_settings = stem.util.conf.uses_settings('stem_interpreter', settings_path)
+
+
+ at uses_settings
+def msg(message, config, **attr):
+  return config.get(message).format(**attr)
+
+
+def main():
+  import readline
+
+  import stem.interpreter.arguments
+  import stem.interpreter.autocomplete
+  import stem.interpreter.commands
+
+  try:
+    args = stem.interpreter.arguments.parse(sys.argv[1:])
+  except ValueError as exc:
+    print exc
+    sys.exit(1)
+
+  if args.print_help:
+    print stem.interpreter.arguments.get_help()
+    sys.exit()
+
+  if args.disable_color:
+    global PROMPT
+    stem.util.term.DISABLE_COLOR_SUPPORT = True
+    PROMPT = '>>> '
+
+  # If the user isn't connecting to something in particular then offer to start
+  # tor if it isn't running.
+
+  if not (args.user_provided_port or args.user_provided_socket):
+    is_tor_running = stem.util.system.is_running('tor') or stem.util.system.is_running('tor.real')
+
+    if not is_tor_running:
+      if not stem.util.system.is_available('tor'):
+        print format(msg('msg.tor_unavailable'), *ERROR_OUTPUT)
+        sys.exit(1)
+      else:
+        print format(msg('msg.starting_tor'), *HEADER_OUTPUT)
+
+        stem.process.launch_tor_with_config(
+          config = {
+            'SocksPort': '0',
+            'ControlPort': str(args.control_port),
+            'CookieAuthentication': '1',
+            'ExitPolicy': 'reject *:*',
+          },
+          completion_percent = 5,
+          take_ownership = True,
+        )
+
+  control_port = None if args.user_provided_socket else (args.control_address, args.control_port)
+  control_socket = None if args.user_provided_port else args.control_socket
+
+  controller = stem.connection.connect(
+    control_port = control_port,
+    control_socket = control_socket,
+    password_prompt = True,
+  )
+
+  if controller is None:
+    sys.exit(1)
+
+  with controller:
+    autocompleter = stem.interpreter.autocomplete.Autocompleter(controller)
+    readline.parse_and_bind('tab: complete')
+    readline.set_completer(autocompleter.complete)
+    readline.set_completer_delims('\n')
+
+    interpreter = stem.interpreter.commands.ControlInterpretor(controller)
+
+    for line in msg('msg.startup_banner').splitlines():
+      line_format = HEADER_BOLD_OUTPUT if line.startswith('  ') else HEADER_OUTPUT
+      print format(line, *line_format)
+
+    print
+
+    while True:
+      try:
+        prompt = '... ' if interpreter.is_multiline_context else PROMPT
+        user_input = raw_input(prompt)
+        response = interpreter.run_command(user_input)
+
+        if response is not None:
+          print response
+      except (KeyboardInterrupt, EOFError, stem.SocketClosed) as exc:
+        print  # move cursor to the following line
+        break
diff --git a/stem/interpreter/arguments.py b/stem/interpreter/arguments.py
new file mode 100644
index 0000000..d62a386
--- /dev/null
+++ b/stem/interpreter/arguments.py
@@ -0,0 +1,96 @@
+# Copyright 2014, Damian Johnson and The Tor Project
+# See LICENSE for licensing information
+
+"""
+Commandline argument parsing for our interpreter prompt.
+"""
+
+import collections
+import getopt
+
+import stem.interpreter
+import stem.util.connection
+
+DEFAULT_ARGS = {
+  'control_address': '127.0.0.1',
+  'control_port': 9051,
+  'user_provided_port': False,
+  'control_socket': '/var/run/tor/control',
+  'user_provided_socket': False,
+  'disable_color': False,
+  'print_help': False,
+}
+
+OPT = 'i:s:h'
+
+OPT_EXPANDED = [
+  'interface=',
+  'socket=',
+  'no-color',
+  'help',
+]
+
+
+def parse(argv):
+  """
+  Parses our arguments, providing a named tuple with their values.
+
+  :param list argv: input arguments to be parsed
+
+  :returns: a **named tuple** with our parsed arguments
+
+  :raises: **ValueError** if we got an invalid argument
+  """
+
+  args = dict(DEFAULT_ARGS)
+
+  try:
+    getopt_results = getopt.getopt(argv, OPT, OPT_EXPANDED)[0]
+  except getopt.GetoptError as exc:
+    raise ValueError('%s (for usage provide --help)' % exc)
+
+  for opt, arg in getopt_results:
+    if opt in ('-i', '--interface'):
+      if ':' in arg:
+        address, port = arg.split(':', 1)
+      else:
+        address, port = None, arg
+
+      if address is not None:
+        if not stem.util.connection.is_valid_ipv4_address(address):
+          raise ValueError("'%s' isn't a valid IPv4 address" % address)
+
+        args['control_address'] = address
+
+      if not stem.util.connection.is_valid_port(port):
+        raise ValueError("'%s' isn't a valid port number" % port)
+
+      args['control_port'] = int(port)
+      args['user_provided_port'] = True
+    elif opt in ('-s', '--socket'):
+      args['control_socket'] = arg
+      args['user_provided_socket'] = True
+    elif opt == '--no-color':
+      args['disable_color'] = True
+    elif opt in ('-h', '--help'):
+      args['print_help'] = True
+
+  # translates our args dict into a named tuple
+
+  Args = collections.namedtuple('Args', args.keys())
+  return Args(**args)
+
+
+def get_help():
+  """
+  Provides our --help usage information.
+
+  :returns: **str** with our usage information
+  """
+
+  return stem.interpreter.msg(
+    'msg.help',
+    address = DEFAULT_ARGS['control_address'],
+    port = DEFAULT_ARGS['control_port'],
+    socket = DEFAULT_ARGS['control_socket'],
+  )
diff --git a/stem/interpreter/autocomplete.py b/stem/interpreter/autocomplete.py
new file mode 100644
index 0000000..3a9b40b
--- /dev/null
+++ b/stem/interpreter/autocomplete.py
@@ -0,0 +1,112 @@
+"""
+Tab completion for our interpreter prompt.
+"""
+
+from stem.interpreter import uses_settings
+
+try:
+  # added in python 3.2
+  from functools import lru_cache
+except ImportError:
+  from stem.util.lru_cache import lru_cache
+
+
+ at uses_settings
+def _get_commands(controller, config):
+  """
+  Provides commands recognized by tor.
+  """
+
+  commands = config.get('autocomplete', [])
+
+  if controller is None:
+    return commands
+
+  # GETINFO commands. Lines are of the form '[option] -- [description]'. This
+  # strips '*' from options that accept values.
+
+  results = controller.get_info('info/names', None)
+
+  if results:
+    for line in results.splitlines():
+      option = line.split(' ', 1)[0].rstrip('*')
+      commands.append('GETINFO %s' % option)
+  else:
+    commands.append('GETINFO ')
+
+  # GETCONF, SETCONF, and RESETCONF commands. Lines are of the form
+  # '[option] [type]'.
+
+  results = controller.get_info('config/names', None)
+
+  if results:
+    for line in results.splitlines():
+      option = line.split(' ', 1)[0]
+
+      commands.append('GETCONF %s' % option)
+      commands.append('SETCONF %s' % option)
+      commands.append('RESETCONF %s' % option)
+  else:
+    commands += ['GETCONF ', 'SETCONF ', 'RESETCONF ']
+
+  # SETEVENT, USEFEATURE, and SIGNAL commands. For each of these the GETINFO
+  # results are simply a space separated lists of the values they can have.
+
+  options = (
+    ('SETEVENTS ', 'events/names'),
+    ('USEFEATURE ', 'features/names'),
+    ('SIGNAL ', 'signal/names'),
+  )
+
+  for prefix, getinfo_cmd in options:
+    results = controller.get_info(getinfo_cmd, None)
+
+    if results:
+      commands += [prefix + value for value in results.split()]
+    else:
+      commands.append(prefix)
+
+  # Adds /help commands.
+
+  usage_info = config.get('help.usage', {})
+
+  for cmd in usage_info.keys():
+    commands.append('/help ' + cmd)
+
+  return commands
+
+
+class Autocompleter(object):
+  def __init__(self, controller):
+    self._commands = _get_commands(controller)
+
+  @lru_cache()
+  def matches(self, text):
+    """
+    Provides autocompletion matches for the given text.
+
+    :param str text: text to check for autocompletion matches with
+
+    :returns: **list** with possible matches
+    """
+
+    lowercase_text = text.lower()
+    return [cmd for cmd in self._commands if cmd.lower().startswith(lowercase_text)]
+
+  def complete(self, text, state):
+    """
+    Provides case insensetive autocompletion options, acting as a functor for
+    the readlines set_completer function.
+
+    :param str text: text to check for autocompletion matches with
+    :param int state: index of result to be provided, readline fetches matches
+      until this function provides None
+
+    :returns: **str** with the autocompletion match, **None** if eithe none
+      exists or state is higher than our number of matches
+    """
+
+    try:
+      return self.matches(text)[state]
+    except IndexError:
+      return None
diff --git a/stem/interpreter/commands.py b/stem/interpreter/commands.py
new file mode 100644
index 0000000..28b3e59
--- /dev/null
+++ b/stem/interpreter/commands.py
@@ -0,0 +1,299 @@
+"""
+Handles making requests and formatting the responses.
+"""
+
+import code
+
+import stem
+import stem.control
+import stem.interpreter.help
+import stem.util.connection
+import stem.util.tor_tools
+
+from stem.interpreter import STANDARD_OUTPUT, BOLD_OUTPUT, ERROR_OUTPUT, uses_settings, msg
+from stem.util.term import format
+
+
+def _get_fingerprint(arg, controller):
+  """
+  Resolves user input into a relay fingerprint. This accepts...
+
+    * Fingerprints
+    * Nicknames
+    * IPv4 addresses, either with or without an ORPort
+    * Empty input, which is resolved to ourselves if we're a relay
+
+  :param str arg: input to be resolved to a relay fingerprint
+  :param stem.control.Controller controller: tor control connection
+
+  :returns: **str** for the relay fingerprint
+
+  :raises: **ValueError** if we're unable to resolve the input to a relay
+  """
+
+  if not arg:
+    try:
+      return controller.get_info('fingerprint')
+    except:
+      raise ValueError("We aren't a relay, no information to provide")
+  elif stem.util.tor_tools.is_valid_fingerprint(arg):
+    return arg
+  elif stem.util.tor_tools.is_valid_nickname(arg):
+    try:
+      return controller.get_network_status(arg).fingerprint
+    except:
+      raise ValueError("Unable to find a relay with the nickname of '%s'" % arg)
+  elif ':' in arg or stem.util.connection.is_valid_ipv4_address(arg):
+    if ':' in arg:
+      address, port = arg.split(':', 1)
+
+      if not stem.util.connection.is_valid_ipv4_address(address):
+        raise ValueError("'%s' isn't a valid IPv4 address" % address)
+      elif port and not stem.util.connection.is_valid_port(port):
+        raise ValueError("'%s' isn't a valid port" % port)
+
+      port = int(port)
+    else:
+      address, port = arg, None
+
+    matches = {}
+
+    for desc in controller.get_network_statuses():
+      if desc.address == address:
+        if not port or desc.or_port == port:
+          matches[desc.or_port] = desc.fingerprint
+
+    if len(matches) == 0:
+      raise ValueError('No relays found at %s' % arg)
+    elif len(matches) == 1:
+      return matches.values()[0]
+    else:
+      response = "There's multiple relays at %s, include a port to specify which.\n\n" % arg
+
+      for i, or_port in enumerate(matches):
+        response += '  %i. %s:%s, fingerprint: %s\n' % (i + 1, address, or_port, matches[or_port])
+
+      raise ValueError(response)
+  else:
+    raise ValueError("'%s' isn't a fingerprint, nickname, or IP address" % arg)
+
+
+class ControlInterpretor(code.InteractiveConsole):
+  """
+  Handles issuing requests and providing nicely formed responses, with support
+  for special irc style subcommands.
+  """
+
+  def __init__(self, controller):
+    self._received_events = []
+
+    code.InteractiveConsole.__init__(self, {
+      'stem': stem,
+      'stem.control': stem.control,
+      'controller': controller,
+      'events': self._received_events
+    })
+
+    self._controller = controller
+    self._run_python_commands = True
+
+    # Indicates if we're processing a multiline command, such as conditional
+    # block or loop.
+
+    self.is_multiline_context = False
+
+    # Intercept events our controller hears about at a pretty low level since
+    # the user will likely be requesting them by direct 'SETEVENTS' calls.
+
+    handle_event_real = self._controller._handle_event
+
+    def handle_event_wrapper(event_message):
+      handle_event_real(event_message)
+      self._received_events.append(event_message)
+
+    self._controller._handle_event = handle_event_wrapper
+
+  def do_help(self, arg):
+    """
+    Performs the '/help' operation, giving usage information for the given
+    argument or a general summary if there wasn't one.
+    """
+
+    return stem.interpreter.help.response(self._controller, arg)
+
+  def do_events(self, arg):
+    """
+    Performs the '/events' operation, dumping the events that we've received
+    belonging to the given types. If no types are specified then this provides
+    all buffered events.
+    """
+
+    events = self._received_events
+    event_types = arg.upper().split()
+
+    if event_types:
+      events = filter(lambda event: event.type in event_types, events)
+
+    return '\n'.join([format(str(event), *STANDARD_OUTPUT) for event in events])
+
+  def do_info(self, arg):
+    """
+    Performs the '/info' operation, looking up a relay by fingerprint, IP
+    address, or nickname and printing its descriptor and consensus entries in a
+    pretty fashion.
+    """
+
+    try:
+      fingerprint = _get_fingerprint(arg, self._controller)
+    except ValueError as exc:
+      return format(str(exc), *ERROR_OUTPUT)
+
+    micro_desc = self._controller.get_microdescriptor(fingerprint, None)
+    server_desc = self._controller.get_server_descriptor(fingerprint, None)
+    ns_desc = self._controller.get_network_status(fingerprint, None)
+
+    # We'll mostly rely on the router status entry. Either the server
+    # descriptor or microdescriptor will be missing, so we'll treat them as
+    # being optional.
+
+    if not ns_desc:
+      return format("Unable to find consensus information for %s" % fingerprint, *ERROR_OUTPUT)
+
+    locale = self._controller.get_info('ip-to-country/%s' % ns_desc.address, None)
+    locale_label = ' (%s)' % locale if locale else ''
+
+    if server_desc:
+      exit_policy_label = server_desc.exit_policy.summary()
+    elif micro_desc:
+      exit_policy_label = micro_desc.exit_policy.summary()
+    else:
+      exit_policy_label = 'Unknown'
+
+    lines = [
+      '%s (%s)' % (ns_desc.nickname, fingerprint),
+      format('address: ', *BOLD_OUTPUT) + '%s:%s%s' % (ns_desc.address, ns_desc.or_port, locale_label),
+      format('published: ', *BOLD_OUTPUT) + ns_desc.published.strftime('%H:%M:%S %d/%m/%Y'),
+    ]
+
+    if server_desc:
+      lines.append(format('os: ', *BOLD_OUTPUT) + server_desc.platform.decode('utf-8', 'replace'))
+      lines.append(format('version: ', *BOLD_OUTPUT) + str(server_desc.tor_version))
+
+    lines.append(format('flags: ', *BOLD_OUTPUT) + ', '.join(ns_desc.flags))
+    lines.append(format('exit policy: ', *BOLD_OUTPUT) + exit_policy_label)
+
+    if server_desc:
+      contact = server_desc.contact
+
+      # clears up some highly common obscuring
+
+      for alias in (' at ', ' AT '):
+        contact = contact.replace(alias, '@')
+
+      for alias in (' dot ', ' DOT '):
+        contact = contact.replace(alias, '.')
+
+      lines.append(format('contact: ', *BOLD_OUTPUT) + contact)
+
+    return '\n'.join(lines)
+
+  def do_python(self, arg):
+    """
+    Performs the '/python' operation, toggling if we accept python commands or
+    not.
+    """
+
+    if not arg:
+      status = 'enabled' if self._run_python_commands else 'disabled'
+      return format('Python support is presently %s.' % status, *STANDARD_OUTPUT)
+    elif arg.lower() == 'enable':
+      self._run_python_commands = True
+    elif arg.lower() == 'disable':
+      self._run_python_commands = False
+    else:
+      return format("'%s' is not recognized. Please run either '/python enable' or '/python disable'." % arg, *ERROR_OUTPUT)
+
+    if self._run_python_commands:
+      response = "Python support enabled, we'll now run non-interpreter commands as python."
+    else:
+      response = "Python support disabled, we'll now pass along all commands to tor."
+
+    return format(response, *STANDARD_OUTPUT)
+
+  @uses_settings
+  def run_command(self, command, config):
+    """
+    Runs the given command. Requests starting with a '/' are special commands
+    to the interpreter, and anything else is sent to the control port.
+
+    :param stem.control.Controller controller: tor control connection
+    :param str command: command to be processed
+
+    :returns: **list** out output lines, each line being a list of
+      (msg, format) tuples
+
+    :raises: **stem.SocketClosed** if the control connection has been severed
+    """
+
+    if not self._controller.is_alive():
+      raise stem.SocketClosed()
+
+    # Commands fall into three categories:
+    #
+    # * Interpretor commands. These start with a '/'.
+    #
+    # * Controller commands stem knows how to handle. We use our Controller's
+    #   methods for these to take advantage of caching and present nicer
+    #   output.
+    #
+    # * Other tor commands. We pass these directly on to the control port.
+
+    cmd, arg = command.strip(), ''
+
+    if ' ' in cmd:
+      cmd, arg = cmd.split(' ', 1)
+
+    output = ''
+
+    if cmd.startswith('/'):
+      cmd = cmd.lower()
+
+      if cmd == '/quit':
+        raise stem.SocketClosed()
+      elif cmd == '/events':
+        output = self.do_events(arg)
+      elif cmd == '/info':
+        output = self.do_info(arg)
+      elif cmd == '/python':
+        output = self.do_python(arg)
+      elif cmd == '/help':
+        output = self.do_help(arg)
+      else:
+        output = format("'%s' isn't a recognized command" % command, *ERROR_OUTPUT)
+    else:
+      cmd = cmd.upper()  # makes commands uppercase to match the spec
+
+      if cmd.replace('+', '') in ('LOADCONF', 'POSTDESCRIPTOR'):
+        # provides a notice that multi-line controller input isn't yet implemented
+        output = format(msg('msg.multiline_unimplemented_notice'), *ERROR_OUTPUT)
+      elif cmd == 'QUIT':
+        self._controller.msg(command)
+        raise stem.SocketClosed()
+      else:
+        is_tor_command = cmd in config.get('help.usage', {}) and cmd.lower() != 'events'
+
+        if self._run_python_commands and not is_tor_command:
+          self.is_multiline_context = code.InteractiveConsole.push(self, command)
+          return
+        else:
+          try:
+            output = format(self._controller.msg(command).raw_content().strip(), *STANDARD_OUTPUT)
+          except stem.ControllerError as exc:
+            if isinstance(exc, stem.SocketClosed):
+              raise exc
+            else:
+              output = format(str(exc), *ERROR_OUTPUT)
+
+    output += '\n'  # give ourselves an extra line before the next prompt
+
+    return output
diff --git a/stem/interpreter/help.py b/stem/interpreter/help.py
new file mode 100644
index 0000000..e17fe3f
--- /dev/null
+++ b/stem/interpreter/help.py
@@ -0,0 +1,142 @@
+"""
+Provides our /help responses.
+"""
+
+from stem.interpreter import (
+  STANDARD_OUTPUT,
+  BOLD_OUTPUT,
+  ERROR_OUTPUT,
+  msg,
+  uses_settings,
+)
+
+from stem.util.term import format
+
+try:
+  # added in python 3.2
+  from functools import lru_cache
+except ImportError:
+  from stem.util.lru_cache import lru_cache
+
+
+def response(controller, arg):
+  """
+  Provides our /help response.
+
+  :param stem.control.Controller controller: tor control connection
+  :param str arg: controller or interpreter command to provide help output for
+
+  :returns: **str** with our help response
+  """
+
+  # Normalizing inputs first so we can better cache responses.
+
+  return _response(controller, _normalize(arg))
+
+
+def _normalize(arg):
+  arg = arg.upper()
+
+  # If there's multiple arguments then just take the first. This is
+  # particularly likely if they're trying to query a full command (for
+  # instance "/help GETINFO version")
+
+  arg = arg.split(' ')[0]
+
+  # strip slash if someone enters an interpreter command (ex. "/help /help")
+
+  if arg.startswith('/'):
+    arg = arg[1:]
+
+  return arg
+
+
+ at lru_cache()
+ at uses_settings
+def _response(controller, arg, config):
+  if not arg:
+    return _general_help()
+
+  usage_info = config.get('help.usage', {})
+
+  if not arg in usage_info:
+    return format("No help information available for '%s'..." % arg, *ERROR_OUTPUT)
+
+  output = format(usage_info[arg] + '\n', *BOLD_OUTPUT)
+
+  description = config.get('help.description.%s' % arg.lower(), '')
+
+  for line in description.splitlines():
+    output += format('  ' + line, *STANDARD_OUTPUT) + '\n'
+
+  output += '\n'
+
+  if arg == 'GETINFO':
+    results = controller.get_info('info/names', None)
+
+    if results:
+      for line in results.splitlines():
+        if ' -- ' in line:
+          opt, summary = line.split(' -- ', 1)
+
+          output += format('%-33s' % opt, *BOLD_OUTPUT)
+          output += format(' - %s' % summary, *STANDARD_OUTPUT) + '\n'
+  elif arg == 'GETCONF':
+    results = controller.get_info('config/names', None)
+
+    if results:
+      options = [opt.split(' ', 1)[0] for opt in results.splitlines()]
+
+      for i in range(0, len(options), 2):
+        line = ''
+
+        for entry in options[i:i + 2]:
+          line += '%-42s' % entry
+
+        output += format(line.rstrip(), *STANDARD_OUTPUT) + '\n'
+  elif arg == 'SIGNAL':
+    signal_options = config.get('help.signal.options', {})
+
+    for signal, summary in signal_options.items():
+      output += format('%-15s' % signal, *BOLD_OUTPUT)
+      output += format(' - %s' % summary, *STANDARD_OUTPUT) + '\n'
+  elif arg == 'SETEVENTS':
+    results = controller.get_info('events/names', None)
+
+    if results:
+      entries = results.split()
+
+      # displays four columns of 20 characters
+
+      for i in range(0, len(entries), 4):
+        line = ''
+
+        for entry in entries[i:i + 4]:
+          line += '%-20s' % entry
+
+        output += format(line.rstrip(), *STANDARD_OUTPUT) + '\n'
+  elif arg == 'USEFEATURE':
+    results = controller.get_info('features/names', None)
+
+    if results:
+      output += format(results, *STANDARD_OUTPUT) + '\n'
+  elif arg in ('LOADCONF', 'POSTDESCRIPTOR'):
+    # gives a warning that this option isn't yet implemented
+    output += format(msg('msg.multiline_unimplemented_notice'), *ERROR_OUTPUT) + '\n'
+
+  return output.rstrip()
+
+
+def _general_help():
+  lines = []
+
+  for line in msg('help.general').splitlines():
+    div = line.find(' - ')
+
+    if div != -1:
+      cmd, description = line[:div], line[div:]
+      lines.append(format(cmd, *BOLD_OUTPUT) + format(description, *STANDARD_OUTPUT))
+    else:
+      lines.append(format(line, *BOLD_OUTPUT))
+
+  return '\n'.join(lines)
diff --git a/stem/interpreter/settings.cfg b/stem/interpreter/settings.cfg
new file mode 100644
index 0000000..0ddf080
--- /dev/null
+++ b/stem/interpreter/settings.cfg
@@ -0,0 +1,295 @@
+################################################################################
+#
+# Configuration data used by Stem's interpreter prompt.
+#
+################################################################################
+
+ ##################
+# GENERAL MESSAGES #
+ ##################
+
+msg.multiline_unimplemented_notice Multi-line control options like this are not yet implemented.
+
+msg.help
+|Interactive interpreter for Tor. This provides you with direct access
+|to Tor's control interface via either python or direct requests.
+|
+|  -i, --interface [ADDRESS:]PORT  change control interface from {address}:{port}
+|  -s, --socket SOCKET_PATH        attach using unix domain socket if present,
+|                                    SOCKET_PATH defaults to: {socket}
+|  --no-color                      disables colorized output
+|  -h, --help                      presents this help
+|
+
+msg.startup_banner
+|Welcome to Stem's interpreter prompt. This provides you with direct access to
+|Tor's control interface.
+|
+|This acts like a standard python interpreter with a Tor connection available
+|via your 'controller' variable...
+|
+|  >>> controller.get_info('version')
+|  '0.2.5.1-alpha-dev (git-245ecfff36c0cecc)'
+|
+|You can also issue requests directly to Tor...
+|
+|  >>> GETINFO version
+|  250-version=0.2.5.1-alpha-dev (git-245ecfff36c0cecc)
+|  250 OK
+|
+|For more information run '/help'.
+|
+
+msg.tor_unavailable Tor isn't running and the command presently isn't in your PATH.
+
+msg.starting_tor
+|Tor isn't running. Starting a temporary Tor instance for our interpreter to
+|interact with. This will have a minimal non-relaying configuration, and be
+|shut down when you're done.
+|
+|--------------------------------------------------------------------------------
+|
+
+ #################
+# OUTPUT OF /HELP #
+ #################
+
+# Response for the '/help' command without any arguments.
+
+help.general
+|Interpretor commands include:
+|  /help   - provides information for interpreter and tor commands
+|  /events - prints events that we've received
+|  /info   - general information for a relay
+|  /python - enable or disable support for running python commands
+|  /quit   - shuts down the interpreter
+|
+|Tor commands include:
+|  GETINFO - queries information from tor
+|  GETCONF, SETCONF, RESETCONF - show or edit a configuration option
+|  SIGNAL - issues control signal to the process (for resetting, stopping, etc)
+|  SETEVENTS - configures the events tor will notify us of
+|
+|  USEFEATURE - enables custom behavior for the controller
+|  SAVECONF - writes tor's current configuration to our torrc
+|  LOADCONF - loads the given input like it was part of our torrc
+|  MAPADDRESS - replaces requests for one address with another
+|  POSTDESCRIPTOR - adds a relay descriptor to our cache
+|  EXTENDCIRCUIT - create or extend a tor circuit
+|  SETCIRCUITPURPOSE - configures the purpose associated with a circuit
+|  CLOSECIRCUIT - closes the given circuit
+|  ATTACHSTREAM - associates an application's stream with a tor circuit
+|  REDIRECTSTREAM - sets a stream's destination
+|  CLOSESTREAM - closes the given stream
+|  RESOLVE - issues an asynchronous dns or rdns request over tor
+|  TAKEOWNERSHIP - instructs tor to quit when this control connection is closed
+|  PROTOCOLINFO - queries version and controller authentication information
+|  QUIT - disconnect the control connection
+|
+|For more information use '/help [OPTION]'.
+
+# Usage of tor and interpreter commands.
+
+help.usage HELP => /help [OPTION]
+help.usage EVENTS => /events [types]
+help.usage INFO => /info [relay fingerprint, nickname, or IP address]
+help.usage PYTHON => /python [enable,disable]
+help.usage QUIT => /quit
+help.usage GETINFO => GETINFO OPTION
+help.usage GETCONF => GETCONF OPTION
+help.usage SETCONF => SETCONF PARAM[=VALUE]
+help.usage RESETCONF => RESETCONF PARAM[=VALUE]
+help.usage SIGNAL => SIGNAL SIG
+help.usage SETEVENTS => SETEVENTS [EXTENDED] [EVENTS]
+help.usage USEFEATURE => USEFEATURE OPTION
+help.usage SAVECONF => SAVECONF
+help.usage LOADCONF => LOADCONF...
+help.usage MAPADDRESS => MAPADDRESS SOURCE_ADDR=DESTINATION_ADDR
+help.usage POSTDESCRIPTOR => POSTDESCRIPTOR [purpose=general/controller/bridge] [cache=yes/no]...
+help.usage EXTENDCIRCUIT => EXTENDCIRCUIT CircuitID [PATH] [purpose=general/controller]
+help.usage SETCIRCUITPURPOSE => SETCIRCUITPURPOSE CircuitID purpose=general/controller
+help.usage CLOSECIRCUIT => CLOSECIRCUIT CircuitID [IfUnused]
+help.usage ATTACHSTREAM => ATTACHSTREAM StreamID CircuitID [HOP=HopNum]
+help.usage REDIRECTSTREAM => REDIRECTSTREAM StreamID Address [Port]
+help.usage CLOSESTREAM => CLOSESTREAM StreamID Reason [Flag]
+help.usage RESOLVE => RESOLVE [mode=reverse] address
+help.usage TAKEOWNERSHIP => TAKEOWNERSHIP
+help.usage PROTOCOLINFO => PROTOCOLINFO [ProtocolVersion]
+
+# Longer description of what tor and interpreter commands do.
+
+help.description.help
+|Provides usage information for the given interpreter, tor command, or tor
+|configuration option.
+|
+|Example:
+|  /help info        # provides a description of the '/info' option
+|  /help GETINFO     # usage information for tor's GETINFO controller option
+
+help.description.events
+|Provides events that we've received belonging to the given event types. If
+|no types are specified then this provides all the messages that we've
+|received.
+
+help.description.info
+|Provides general information for a relay that's currently in the consensus.
+|If no relay is specified then this provides information on ourselves.
+
+help.description.python
+|Enables or disables support for running python commands. This determines how
+|we treat commands this interpreter doesn't recognize...
+|
+|* If enabled then unrecognized commands are executed as python.
+|* If disabled then unrecognized commands are passed along to tor.
+
+help.description.quit
+|Terminates the interpreter.
+
+help.description.getinfo
+|Queries the tor process for information. Options are...
+|
+
+help.description.getconf
+|Provides the current value for a given configuration value. Options include...
+|
+
+help.description.setconf
+|Sets the given configuration parameters. Values can be quoted or non-quoted
+|strings, and reverts the option to 0 or NULL if not provided.
+|
+|Examples:
+|  * Sets a contact address and resets our family to NULL
+|    SETCONF MyFamily ContactInfo=foo at bar.com
+|
+|  * Sets an exit policy that only includes port 80/443
+|    SETCONF ExitPolicy=\"accept *:80, accept *:443, reject *:*\"\
+
+help.description.resetconf
+|Reverts the given configuration options to their default values. If a value
+|is provided then this behaves in the same way as SETCONF.
+|
+|Examples:
+|  * Returns both of our accounting parameters to their defaults
+|    RESETCONF AccountingMax AccountingStart
+|
+|  * Uses the default exit policy and sets our nickname to be 'Goomba'
+|    RESETCONF ExitPolicy Nickname=Goomba
+
+help.description.signal
+|Issues a signal that tells the tor process to reload its torrc, dump its
+|stats, halt, etc.
+
+help.description.setevents
+|Sets the events that we will receive. This turns off any events that aren't
+|listed so sending 'SETEVENTS' without any values will turn off all event reporting.
+|
+|For Tor versions between 0.1.1.9 and 0.2.2.1 adding 'EXTENDED' causes some
+|events to give us additional information. After version 0.2.2.1 this is
+|always on.
+|
+|Events include...
+|
+
+help.description.usefeature
+|Customizes the behavior of the control port. Options include...
+|
+
+help.description.saveconf
+|Writes Tor's current configuration to its torrc.
+
+help.description.loadconf
+|Reads the given text like it belonged to our torrc.
+|
+|Example:
+|  +LOADCONF
+|  # sets our exit policy to just accept ports 80 and 443
+|  ExitPolicy accept *:80
+|  ExitPolicy accept *:443
+|  ExitPolicy reject *:*
+|  .
+
+help.description.mapaddress
+|Replaces future requests for one address with another.
+|
+|Example:
+|  MAPADDRESS 0.0.0.0=torproject.org 1.2.3.4=tor.freehaven.net
+
+help.description.postdescriptor
+|Simulates getting a new relay descriptor.
+
+help.description.extendcircuit
+|Extends the given circuit or create a new one if the CircuitID is zero. The
+|PATH is a comma separated list of fingerprints. If it isn't set then this
+|uses Tor's normal path selection.
+
+help.description.setcircuitpurpose
+|Sets the purpose attribute for a circuit.
+
+help.description.closecircuit
+|Closes the given circuit. If "IfUnused" is included then this only closes
+|the circuit if it isn't currently being used.
+
+help.description.attachstream
+|Attaches a stream with the given built circuit (tor picks one on its own if
+|CircuitID is zero). If HopNum is given then this hop is used to exit the
+|circuit, otherwise the last relay is used.
+
+help.description.redirectstream
+|Sets the destination for a given stream. This can only be done after a
+|stream is created but before it's attached to a circuit.
+
+help.description.closestream
+|Closes the given stream, the reason being an integer matching a reason as
+|per section 6.3 of the tor-spec.
+
+help.description.resolve
+|Performs IPv4 DNS resolution over tor, doing a reverse lookup instead if
+|"mode=reverse" is included. This request is processed in the background and
+|results in a ADDRMAP event with the response.
+
+help.description.takeownership
+|Instructs Tor to gracefully shut down when this control connection is closed.
+
+help.description.protocolinfo
+|Provides bootstrapping information that a controller might need when first
+|starting, like Tor's version and controller authentication. This can be done
+|before authenticating to the control port.
+
+help.signal.options RELOAD / HUP => reload our torrc
+help.signal.options SHUTDOWN / INT => gracefully shut down, waiting 30 seconds if we're a relay
+help.signal.options DUMP / USR1 => logs information about open connections and circuits
+help.signal.options DEBUG / USR2 => makes us log at the DEBUG runlevel
+help.signal.options HALT / TERM => immediately shut down
+help.signal.options CLEARDNSCACHE => clears any cached DNS results
+help.signal.options NEWNYM => clears the DNS cache and uses new circuits for future connections
+
+ ################
+# TAB COMPLETION #
+ ################
+
+# Commands we'll autocomplete when the user hits tab. This is just the start of
+# our autocompletion list - more are determined dynamically by checking what
+# tor supports.
+
+autocomplete /help
+autocomplete /events
+autocomplete /info
+autocomplete /quit
+autocomplete SAVECONF
+autocomplete MAPADDRESS
+autocomplete EXTENDCIRCUIT
+autocomplete SETCIRCUITPURPOSE
+autocomplete SETROUTERPURPOSE
+autocomplete ATTACHSTREAM
+#autocomplete +POSTDESCRIPTOR  # TODO: needs multi-line support
+autocomplete REDIRECTSTREAM
+autocomplete CLOSESTREAM
+autocomplete CLOSECIRCUIT
+autocomplete QUIT
+autocomplete RESOLVE
+autocomplete PROTOCOLINFO
+#autocomplete +LOADCONF  # TODO: needs multi-line support
+autocomplete TAKEOWNERSHIP
+autocomplete AUTHCHALLENGE
+autocomplete DROPGUARDS
+
diff --git a/stem/interpretor/__init__.py b/stem/interpretor/__init__.py
deleted file mode 100644
index 5ecd356..0000000
--- a/stem/interpretor/__init__.py
+++ /dev/null
@@ -1,129 +0,0 @@
-# Copyright 2014, Damian Johnson and The Tor Project
-# See LICENSE for licensing information
-
-"""
-Interactive interpretor for interacting with Tor directly. This adds usability
-features such as tab completion, history, and IRC-style functions (like /help).
-"""
-
-__all__ = ['arguments', 'autocomplete', 'commands', 'help', 'msg']
-
-import os
-import sys
-
-import stem
-import stem.connection
-import stem.process
-import stem.util.conf
-import stem.util.system
-import stem.util.term
-
-from stem.util.term import RESET, Attr, Color, format
-
-# Our color prompt triggers a bug between raw_input() and readline history,
-# where scrolling through history widens our prompt. Widening our prompt via
-# invisible characters (like resets) seems to sidestep this bug for short
-# inputs. Contrary to the ticket, this still manifests with python 2.7.1...
-#
-#   http://bugs.python.org/issue12972
-
-PROMPT = format('>>> ', Color.GREEN, Attr.BOLD) + RESET * 10
-
-STANDARD_OUTPUT = (Color.BLUE, )
-BOLD_OUTPUT = (Color.BLUE, Attr.BOLD)
-HEADER_OUTPUT = (Color.GREEN, )
-HEADER_BOLD_OUTPUT = (Color.GREEN, Attr.BOLD)
-ERROR_OUTPUT = (Attr.BOLD, Color.RED)
-
-settings_path = os.path.join(os.path.dirname(__file__), 'settings.cfg')
-uses_settings = stem.util.conf.uses_settings('stem_interpretor', settings_path)
-
-
- at uses_settings
-def msg(message, config, **attr):
-  return config.get(message).format(**attr)
-
-
-def main():
-  import readline
-
-  import stem.interpretor.arguments
-  import stem.interpretor.autocomplete
-  import stem.interpretor.commands
-
-  try:
-    args = stem.interpretor.arguments.parse(sys.argv[1:])
-  except ValueError as exc:
-    print exc
-    sys.exit(1)
-
-  if args.print_help:
-    print stem.interpretor.arguments.get_help()
-    sys.exit()
-
-  if args.disable_color:
-    global PROMPT
-    stem.util.term.DISABLE_COLOR_SUPPORT = True
-    PROMPT = '>>> '
-
-  # If the user isn't connecting to something in particular then offer to start
-  # tor if it isn't running.
-
-  if not (args.user_provided_port or args.user_provided_socket):
-    is_tor_running = stem.util.system.is_running('tor') or stem.util.system.is_running('tor.real')
-
-    if not is_tor_running:
-      if not stem.util.system.is_available('tor'):
-        print format(msg('msg.tor_unavailable'), *ERROR_OUTPUT)
-        sys.exit(1)
-      else:
-        print format(msg('msg.starting_tor'), *HEADER_OUTPUT)
-
-        stem.process.launch_tor_with_config(
-          config = {
-            'SocksPort': '0',
-            'ControlPort': str(args.control_port),
-            'CookieAuthentication': '1',
-            'ExitPolicy': 'reject *:*',
-          },
-          completion_percent = 5,
-          take_ownership = True,
-        )
-
-  control_port = None if args.user_provided_socket else (args.control_address, args.control_port)
-  control_socket = None if args.user_provided_port else args.control_socket
-
-  controller = stem.connection.connect(
-    control_port = control_port,
-    control_socket = control_socket,
-    password_prompt = True,
-  )
-
-  if controller is None:
-    sys.exit(1)
-
-  with controller:
-    autocompleter = stem.interpretor.autocomplete.Autocompleter(controller)
-    readline.parse_and_bind('tab: complete')
-    readline.set_completer(autocompleter.complete)
-    readline.set_completer_delims('\n')
-
-    interpretor = stem.interpretor.commands.ControlInterpretor(controller)
-
-    for line in msg('msg.startup_banner').splitlines():
-      line_format = HEADER_BOLD_OUTPUT if line.startswith('  ') else HEADER_OUTPUT
-      print format(line, *line_format)
-
-    print
-
-    while True:
-      try:
-        prompt = '... ' if interpretor.is_multiline_context else PROMPT
-        user_input = raw_input(prompt)
-        response = interpretor.run_command(user_input)
-
-        if response is not None:
-          print response
-      except (KeyboardInterrupt, EOFError, stem.SocketClosed) as exc:
-        print  # move cursor to the following line
-        break
diff --git a/stem/interpretor/arguments.py b/stem/interpretor/arguments.py
deleted file mode 100644
index 278d2a0..0000000
--- a/stem/interpretor/arguments.py
+++ /dev/null
@@ -1,96 +0,0 @@
-# Copyright 2014, Damian Johnson and The Tor Project
-# See LICENSE for licensing information
-
-"""
-Commandline argument parsing for our interpretor prompt.
-"""
-
-import collections
-import getopt
-
-import stem.interpretor
-import stem.util.connection
-
-DEFAULT_ARGS = {
-  'control_address': '127.0.0.1',
-  'control_port': 9051,
-  'user_provided_port': False,
-  'control_socket': '/var/run/tor/control',
-  'user_provided_socket': False,
-  'disable_color': False,
-  'print_help': False,
-}
-
-OPT = 'i:s:h'
-
-OPT_EXPANDED = [
-  'interface=',
-  'socket=',
-  'no-color',
-  'help',
-]
-
-
-def parse(argv):
-  """
-  Parses our arguments, providing a named tuple with their values.
-
-  :param list argv: input arguments to be parsed
-
-  :returns: a **named tuple** with our parsed arguments
-
-  :raises: **ValueError** if we got an invalid argument
-  """
-
-  args = dict(DEFAULT_ARGS)
-
-  try:
-    getopt_results = getopt.getopt(argv, OPT, OPT_EXPANDED)[0]
-  except getopt.GetoptError as exc:
-    raise ValueError('%s (for usage provide --help)' % exc)
-
-  for opt, arg in getopt_results:
-    if opt in ('-i', '--interface'):
-      if ':' in arg:
-        address, port = arg.split(':', 1)
-      else:
-        address, port = None, arg
-
-      if address is not None:
-        if not stem.util.connection.is_valid_ipv4_address(address):
-          raise ValueError("'%s' isn't a valid IPv4 address" % address)
-
-        args['control_address'] = address
-
-      if not stem.util.connection.is_valid_port(port):
-        raise ValueError("'%s' isn't a valid port number" % port)
-
-      args['control_port'] = int(port)
-      args['user_provided_port'] = True
-    elif opt in ('-s', '--socket'):
-      args['control_socket'] = arg
-      args['user_provided_socket'] = True
-    elif opt == '--no-color':
-      args['disable_color'] = True
-    elif opt in ('-h', '--help'):
-      args['print_help'] = True
-
-  # translates our args dict into a named tuple
-
-  Args = collections.namedtuple('Args', args.keys())
-  return Args(**args)
-
-
-def get_help():
-  """
-  Provides our --help usage information.
-
-  :returns: **str** with our usage information
-  """
-
-  return stem.interpretor.msg(
-    'msg.help',
-    address = DEFAULT_ARGS['control_address'],
-    port = DEFAULT_ARGS['control_port'],
-    socket = DEFAULT_ARGS['control_socket'],
-  )
diff --git a/stem/interpretor/autocomplete.py b/stem/interpretor/autocomplete.py
deleted file mode 100644
index f42084e..0000000
--- a/stem/interpretor/autocomplete.py
+++ /dev/null
@@ -1,112 +0,0 @@
-"""
-Tab completion for our interpretor prompt.
-"""
-
-from stem.interpretor import uses_settings
-
-try:
-  # added in python 3.2
-  from functools import lru_cache
-except ImportError:
-  from stem.util.lru_cache import lru_cache
-
-
- at uses_settings
-def _get_commands(controller, config):
-  """
-  Provides commands recognized by tor.
-  """
-
-  commands = config.get('autocomplete', [])
-
-  if controller is None:
-    return commands
-
-  # GETINFO commands. Lines are of the form '[option] -- [description]'. This
-  # strips '*' from options that accept values.
-
-  results = controller.get_info('info/names', None)
-
-  if results:
-    for line in results.splitlines():
-      option = line.split(' ', 1)[0].rstrip('*')
-      commands.append('GETINFO %s' % option)
-  else:
-    commands.append('GETINFO ')
-
-  # GETCONF, SETCONF, and RESETCONF commands. Lines are of the form
-  # '[option] [type]'.
-
-  results = controller.get_info('config/names', None)
-
-  if results:
-    for line in results.splitlines():
-      option = line.split(' ', 1)[0]
-
-      commands.append('GETCONF %s' % option)
-      commands.append('SETCONF %s' % option)
-      commands.append('RESETCONF %s' % option)
-  else:
-    commands += ['GETCONF ', 'SETCONF ', 'RESETCONF ']
-
-  # SETEVENT, USEFEATURE, and SIGNAL commands. For each of these the GETINFO
-  # results are simply a space separated lists of the values they can have.
-
-  options = (
-    ('SETEVENTS ', 'events/names'),
-    ('USEFEATURE ', 'features/names'),
-    ('SIGNAL ', 'signal/names'),
-  )
-
-  for prefix, getinfo_cmd in options:
-    results = controller.get_info(getinfo_cmd, None)
-
-    if results:
-      commands += [prefix + value for value in results.split()]
-    else:
-      commands.append(prefix)
-
-  # Adds /help commands.
-
-  usage_info = config.get('help.usage', {})
-
-  for cmd in usage_info.keys():
-    commands.append('/help ' + cmd)
-
-  return commands
-
-
-class Autocompleter(object):
-  def __init__(self, controller):
-    self._commands = _get_commands(controller)
-
-  @lru_cache()
-  def matches(self, text):
-    """
-    Provides autocompletion matches for the given text.
-
-    :param str text: text to check for autocompletion matches with
-
-    :returns: **list** with possible matches
-    """
-
-    lowercase_text = text.lower()
-    return [cmd for cmd in self._commands if cmd.lower().startswith(lowercase_text)]
-
-  def complete(self, text, state):
-    """
-    Provides case insensetive autocompletion options, acting as a functor for
-    the readlines set_completer function.
-
-    :param str text: text to check for autocompletion matches with
-    :param int state: index of result to be provided, readline fetches matches
-      until this function provides None
-
-    :returns: **str** with the autocompletion match, **None** if eithe none
-      exists or state is higher than our number of matches
-    """
-
-    try:
-      return self.matches(text)[state]
-    except IndexError:
-      return None
diff --git a/stem/interpretor/commands.py b/stem/interpretor/commands.py
deleted file mode 100644
index b66c87b..0000000
--- a/stem/interpretor/commands.py
+++ /dev/null
@@ -1,299 +0,0 @@
-"""
-Handles making requests and formatting the responses.
-"""
-
-import code
-
-import stem
-import stem.control
-import stem.interpretor.help
-import stem.util.connection
-import stem.util.tor_tools
-
-from stem.interpretor import STANDARD_OUTPUT, BOLD_OUTPUT, ERROR_OUTPUT, uses_settings, msg
-from stem.util.term import format
-
-
-def _get_fingerprint(arg, controller):
-  """
-  Resolves user input into a relay fingerprint. This accepts...
-
-    * Fingerprints
-    * Nicknames
-    * IPv4 addresses, either with or without an ORPort
-    * Empty input, which is resolved to ourselves if we're a relay
-
-  :param str arg: input to be resolved to a relay fingerprint
-  :param stem.control.Controller controller: tor control connection
-
-  :returns: **str** for the relay fingerprint
-
-  :raises: **ValueError** if we're unable to resolve the input to a relay
-  """
-
-  if not arg:
-    try:
-      return controller.get_info('fingerprint')
-    except:
-      raise ValueError("We aren't a relay, no information to provide")
-  elif stem.util.tor_tools.is_valid_fingerprint(arg):
-    return arg
-  elif stem.util.tor_tools.is_valid_nickname(arg):
-    try:
-      return controller.get_network_status(arg).fingerprint
-    except:
-      raise ValueError("Unable to find a relay with the nickname of '%s'" % arg)
-  elif ':' in arg or stem.util.connection.is_valid_ipv4_address(arg):
-    if ':' in arg:
-      address, port = arg.split(':', 1)
-
-      if not stem.util.connection.is_valid_ipv4_address(address):
-        raise ValueError("'%s' isn't a valid IPv4 address" % address)
-      elif port and not stem.util.connection.is_valid_port(port):
-        raise ValueError("'%s' isn't a valid port" % port)
-
-      port = int(port)
-    else:
-      address, port = arg, None
-
-    matches = {}
-
-    for desc in controller.get_network_statuses():
-      if desc.address == address:
-        if not port or desc.or_port == port:
-          matches[desc.or_port] = desc.fingerprint
-
-    if len(matches) == 0:
-      raise ValueError('No relays found at %s' % arg)
-    elif len(matches) == 1:
-      return matches.values()[0]
-    else:
-      response = "There's multiple relays at %s, include a port to specify which.\n\n" % arg
-
-      for i, or_port in enumerate(matches):
-        response += '  %i. %s:%s, fingerprint: %s\n' % (i + 1, address, or_port, matches[or_port])
-
-      raise ValueError(response)
-  else:
-    raise ValueError("'%s' isn't a fingerprint, nickname, or IP address" % arg)
-
-
-class ControlInterpretor(code.InteractiveConsole):
-  """
-  Handles issuing requests and providing nicely formed responses, with support
-  for special irc style subcommands.
-  """
-
-  def __init__(self, controller):
-    self._received_events = []
-
-    code.InteractiveConsole.__init__(self, {
-      'stem': stem,
-      'stem.control': stem.control,
-      'controller': controller,
-      'events': self._received_events
-    })
-
-    self._controller = controller
-    self._run_python_commands = True
-
-    # Indicates if we're processing a multiline command, such as conditional
-    # block or loop.
-
-    self.is_multiline_context = False
-
-    # Intercept events our controller hears about at a pretty low level since
-    # the user will likely be requesting them by direct 'SETEVENTS' calls.
-
-    handle_event_real = self._controller._handle_event
-
-    def handle_event_wrapper(event_message):
-      handle_event_real(event_message)
-      self._received_events.append(event_message)
-
-    self._controller._handle_event = handle_event_wrapper
-
-  def do_help(self, arg):
-    """
-    Performs the '/help' operation, giving usage information for the given
-    argument or a general summary if there wasn't one.
-    """
-
-    return stem.interpretor.help.response(self._controller, arg)
-
-  def do_events(self, arg):
-    """
-    Performs the '/events' operation, dumping the events that we've received
-    belonging to the given types. If no types are specified then this provides
-    all buffered events.
-    """
-
-    events = self._received_events
-    event_types = arg.upper().split()
-
-    if event_types:
-      events = filter(lambda event: event.type in event_types, events)
-
-    return '\n'.join([format(str(event), *STANDARD_OUTPUT) for event in events])
-
-  def do_info(self, arg):
-    """
-    Performs the '/info' operation, looking up a relay by fingerprint, IP
-    address, or nickname and printing its descriptor and consensus entries in a
-    pretty fashion.
-    """
-
-    try:
-      fingerprint = _get_fingerprint(arg, self._controller)
-    except ValueError as exc:
-      return format(str(exc), *ERROR_OUTPUT)
-
-    micro_desc = self._controller.get_microdescriptor(fingerprint, None)
-    server_desc = self._controller.get_server_descriptor(fingerprint, None)
-    ns_desc = self._controller.get_network_status(fingerprint, None)
-
-    # We'll mostly rely on the router status entry. Either the server
-    # descriptor or microdescriptor will be missing, so we'll treat them as
-    # being optional.
-
-    if not ns_desc:
-      return format("Unable to find consensus information for %s" % fingerprint, *ERROR_OUTPUT)
-
-    locale = self._controller.get_info('ip-to-country/%s' % ns_desc.address, None)
-    locale_label = ' (%s)' % locale if locale else ''
-
-    if server_desc:
-      exit_policy_label = server_desc.exit_policy.summary()
-    elif micro_desc:
-      exit_policy_label = micro_desc.exit_policy.summary()
-    else:
-      exit_policy_label = 'Unknown'
-
-    lines = [
-      '%s (%s)' % (ns_desc.nickname, fingerprint),
-      format('address: ', *BOLD_OUTPUT) + '%s:%s%s' % (ns_desc.address, ns_desc.or_port, locale_label),
-      format('published: ', *BOLD_OUTPUT) + ns_desc.published.strftime('%H:%M:%S %d/%m/%Y'),
-    ]
-
-    if server_desc:
-      lines.append(format('os: ', *BOLD_OUTPUT) + server_desc.platform.decode('utf-8', 'replace'))
-      lines.append(format('version: ', *BOLD_OUTPUT) + str(server_desc.tor_version))
-
-    lines.append(format('flags: ', *BOLD_OUTPUT) + ', '.join(ns_desc.flags))
-    lines.append(format('exit policy: ', *BOLD_OUTPUT) + exit_policy_label)
-
-    if server_desc:
-      contact = server_desc.contact
-
-      # clears up some highly common obscuring
-
-      for alias in (' at ', ' AT '):
-        contact = contact.replace(alias, '@')
-
-      for alias in (' dot ', ' DOT '):
-        contact = contact.replace(alias, '.')
-
-      lines.append(format('contact: ', *BOLD_OUTPUT) + contact)
-
-    return '\n'.join(lines)
-
-  def do_python(self, arg):
-    """
-    Performs the '/python' operation, toggling if we accept python commands or
-    not.
-    """
-
-    if not arg:
-      status = 'enabled' if self._run_python_commands else 'disabled'
-      return format('Python support is presently %s.' % status, *STANDARD_OUTPUT)
-    elif arg.lower() == 'enable':
-      self._run_python_commands = True
-    elif arg.lower() == 'disable':
-      self._run_python_commands = False
-    else:
-      return format("'%s' is not recognized. Please run either '/python enable' or '/python disable'." % arg, *ERROR_OUTPUT)
-
-    if self._run_python_commands:
-      response = "Python support enabled, we'll now run non-interpretor commands as python."
-    else:
-      response = "Python support disabled, we'll now pass along all commands to tor."
-
-    return format(response, *STANDARD_OUTPUT)
-
-  @uses_settings
-  def run_command(self, command, config):
-    """
-    Runs the given command. Requests starting with a '/' are special commands
-    to the interpretor, and anything else is sent to the control port.
-
-    :param stem.control.Controller controller: tor control connection
-    :param str command: command to be processed
-
-    :returns: **list** out output lines, each line being a list of
-      (msg, format) tuples
-
-    :raises: **stem.SocketClosed** if the control connection has been severed
-    """
-
-    if not self._controller.is_alive():
-      raise stem.SocketClosed()
-
-    # Commands fall into three categories:
-    #
-    # * Interpretor commands. These start with a '/'.
-    #
-    # * Controller commands stem knows how to handle. We use our Controller's
-    #   methods for these to take advantage of caching and present nicer
-    #   output.
-    #
-    # * Other tor commands. We pass these directly on to the control port.
-
-    cmd, arg = command.strip(), ''
-
-    if ' ' in cmd:
-      cmd, arg = cmd.split(' ', 1)
-
-    output = ''
-
-    if cmd.startswith('/'):
-      cmd = cmd.lower()
-
-      if cmd == '/quit':
-        raise stem.SocketClosed()
-      elif cmd == '/events':
-        output = self.do_events(arg)
-      elif cmd == '/info':
-        output = self.do_info(arg)
-      elif cmd == '/python':
-        output = self.do_python(arg)
-      elif cmd == '/help':
-        output = self.do_help(arg)
-      else:
-        output = format("'%s' isn't a recognized command" % command, *ERROR_OUTPUT)
-    else:
-      cmd = cmd.upper()  # makes commands uppercase to match the spec
-
-      if cmd.replace('+', '') in ('LOADCONF', 'POSTDESCRIPTOR'):
-        # provides a notice that multi-line controller input isn't yet implemented
-        output = format(msg('msg.multiline_unimplemented_notice'), *ERROR_OUTPUT)
-      elif cmd == 'QUIT':
-        self._controller.msg(command)
-        raise stem.SocketClosed()
-      else:
-        is_tor_command = cmd in config.get('help.usage', {}) and cmd.lower() != 'events'
-
-        if self._run_python_commands and not is_tor_command:
-          self.is_multiline_context = code.InteractiveConsole.push(self, command)
-          return
-        else:
-          try:
-            output = format(self._controller.msg(command).raw_content().strip(), *STANDARD_OUTPUT)
-          except stem.ControllerError as exc:
-            if isinstance(exc, stem.SocketClosed):
-              raise exc
-            else:
-              output = format(str(exc), *ERROR_OUTPUT)
-
-    output += '\n'  # give ourselves an extra line before the next prompt
-
-    return output
diff --git a/stem/interpretor/help.py b/stem/interpretor/help.py
deleted file mode 100644
index b7909a9..0000000
--- a/stem/interpretor/help.py
+++ /dev/null
@@ -1,142 +0,0 @@
-"""
-Provides our /help responses.
-"""
-
-from stem.interpretor import (
-  STANDARD_OUTPUT,
-  BOLD_OUTPUT,
-  ERROR_OUTPUT,
-  msg,
-  uses_settings,
-)
-
-from stem.util.term import format
-
-try:
-  # added in python 3.2
-  from functools import lru_cache
-except ImportError:
-  from stem.util.lru_cache import lru_cache
-
-
-def response(controller, arg):
-  """
-  Provides our /help response.
-
-  :param stem.control.Controller controller: tor control connection
-  :param str arg: controller or interpretor command to provide help output for
-
-  :returns: **str** with our help response
-  """
-
-  # Normalizing inputs first so we can better cache responses.
-
-  return _response(controller, _normalize(arg))
-
-
-def _normalize(arg):
-  arg = arg.upper()
-
-  # If there's multiple arguments then just take the first. This is
-  # particularly likely if they're trying to query a full command (for
-  # instance "/help GETINFO version")
-
-  arg = arg.split(' ')[0]
-
-  # strip slash if someone enters an interpretor command (ex. "/help /help")
-
-  if arg.startswith('/'):
-    arg = arg[1:]
-
-  return arg
-
-
- at lru_cache()
- at uses_settings
-def _response(controller, arg, config):
-  if not arg:
-    return _general_help()
-
-  usage_info = config.get('help.usage', {})
-
-  if not arg in usage_info:
-    return format("No help information available for '%s'..." % arg, *ERROR_OUTPUT)
-
-  output = format(usage_info[arg] + '\n', *BOLD_OUTPUT)
-
-  description = config.get('help.description.%s' % arg.lower(), '')
-
-  for line in description.splitlines():
-    output += format('  ' + line, *STANDARD_OUTPUT) + '\n'
-
-  output += '\n'
-
-  if arg == 'GETINFO':
-    results = controller.get_info('info/names', None)
-
-    if results:
-      for line in results.splitlines():
-        if ' -- ' in line:
-          opt, summary = line.split(' -- ', 1)
-
-          output += format('%-33s' % opt, *BOLD_OUTPUT)
-          output += format(' - %s' % summary, *STANDARD_OUTPUT) + '\n'
-  elif arg == 'GETCONF':
-    results = controller.get_info('config/names', None)
-
-    if results:
-      options = [opt.split(' ', 1)[0] for opt in results.splitlines()]
-
-      for i in range(0, len(options), 2):
-        line = ''
-
-        for entry in options[i:i + 2]:
-          line += '%-42s' % entry
-
-        output += format(line.rstrip(), *STANDARD_OUTPUT) + '\n'
-  elif arg == 'SIGNAL':
-    signal_options = config.get('help.signal.options', {})
-
-    for signal, summary in signal_options.items():
-      output += format('%-15s' % signal, *BOLD_OUTPUT)
-      output += format(' - %s' % summary, *STANDARD_OUTPUT) + '\n'
-  elif arg == 'SETEVENTS':
-    results = controller.get_info('events/names', None)
-
-    if results:
-      entries = results.split()
-
-      # displays four columns of 20 characters
-
-      for i in range(0, len(entries), 4):
-        line = ''
-
-        for entry in entries[i:i + 4]:
-          line += '%-20s' % entry
-
-        output += format(line.rstrip(), *STANDARD_OUTPUT) + '\n'
-  elif arg == 'USEFEATURE':
-    results = controller.get_info('features/names', None)
-
-    if results:
-      output += format(results, *STANDARD_OUTPUT) + '\n'
-  elif arg in ('LOADCONF', 'POSTDESCRIPTOR'):
-    # gives a warning that this option isn't yet implemented
-    output += format(msg('msg.multiline_unimplemented_notice'), *ERROR_OUTPUT) + '\n'
-
-  return output.rstrip()
-
-
-def _general_help():
-  lines = []
-
-  for line in msg('help.general').splitlines():
-    div = line.find(' - ')
-
-    if div != -1:
-      cmd, description = line[:div], line[div:]
-      lines.append(format(cmd, *BOLD_OUTPUT) + format(description, *STANDARD_OUTPUT))
-    else:
-      lines.append(format(line, *BOLD_OUTPUT))
-
-  return '\n'.join(lines)
diff --git a/stem/interpretor/settings.cfg b/stem/interpretor/settings.cfg
deleted file mode 100644
index 028188f..0000000
--- a/stem/interpretor/settings.cfg
+++ /dev/null
@@ -1,295 +0,0 @@
-################################################################################
-#
-# Configuration data used by Stem's interpretor prompt.
-#
-################################################################################
-
- ##################
-# GENERAL MESSAGES #
- ##################
-
-msg.multiline_unimplemented_notice Multi-line control options like this are not yet implemented.
-
-msg.help
-|Interactive interpretor for Tor. This provides you with direct access
-|to Tor's control interface via either python or direct requests.
-|
-|  -i, --interface [ADDRESS:]PORT  change control interface from {address}:{port}
-|  -s, --socket SOCKET_PATH        attach using unix domain socket if present,
-|                                    SOCKET_PATH defaults to: {socket}
-|  --no-color                      disables colorized output
-|  -h, --help                      presents this help
-|
-
-msg.startup_banner
-|Welcome to Stem's interpretor prompt. This provides you with direct access to
-|Tor's control interface.
-|
-|This acts like a standard python interpretor with a Tor connection available
-|via your 'controller' variable...
-|
-|  >>> controller.get_info('version')
-|  '0.2.5.1-alpha-dev (git-245ecfff36c0cecc)'
-|
-|You can also issue requests directly to Tor...
-|
-|  >>> GETINFO version
-|  250-version=0.2.5.1-alpha-dev (git-245ecfff36c0cecc)
-|  250 OK
-|
-|For more information run '/help'.
-|
-
-msg.tor_unavailable Tor isn't running and the command presently isn't in your PATH.
-
-msg.starting_tor
-|Tor isn't running. Starting a temporary Tor instance for our interpretor to
-|interact with. This will have a minimal non-relaying configuration, and be
-|shut down when you're done.
-|
-|--------------------------------------------------------------------------------
-|
-
- #################
-# OUTPUT OF /HELP #
- #################
-
-# Response for the '/help' command without any arguments.
-
-help.general
-|Interpretor commands include:
-|  /help   - provides information for interpretor and tor commands
-|  /events - prints events that we've received
-|  /info   - general information for a relay
-|  /python - enable or disable support for running python commands
-|  /quit   - shuts down the interpretor
-|
-|Tor commands include:
-|  GETINFO - queries information from tor
-|  GETCONF, SETCONF, RESETCONF - show or edit a configuration option
-|  SIGNAL - issues control signal to the process (for resetting, stopping, etc)
-|  SETEVENTS - configures the events tor will notify us of
-|
-|  USEFEATURE - enables custom behavior for the controller
-|  SAVECONF - writes tor's current configuration to our torrc
-|  LOADCONF - loads the given input like it was part of our torrc
-|  MAPADDRESS - replaces requests for one address with another
-|  POSTDESCRIPTOR - adds a relay descriptor to our cache
-|  EXTENDCIRCUIT - create or extend a tor circuit
-|  SETCIRCUITPURPOSE - configures the purpose associated with a circuit
-|  CLOSECIRCUIT - closes the given circuit
-|  ATTACHSTREAM - associates an application's stream with a tor circuit
-|  REDIRECTSTREAM - sets a stream's destination
-|  CLOSESTREAM - closes the given stream
-|  RESOLVE - issues an asynchronous dns or rdns request over tor
-|  TAKEOWNERSHIP - instructs tor to quit when this control connection is closed
-|  PROTOCOLINFO - queries version and controller authentication information
-|  QUIT - disconnect the control connection
-|
-|For more information use '/help [OPTION]'.
-
-# Usage of tor and interpretor commands.
-
-help.usage HELP => /help [OPTION]
-help.usage EVENTS => /events [types]
-help.usage INFO => /info [relay fingerprint, nickname, or IP address]
-help.usage PYTHON => /python [enable,disable]
-help.usage QUIT => /quit
-help.usage GETINFO => GETINFO OPTION
-help.usage GETCONF => GETCONF OPTION
-help.usage SETCONF => SETCONF PARAM[=VALUE]
-help.usage RESETCONF => RESETCONF PARAM[=VALUE]
-help.usage SIGNAL => SIGNAL SIG
-help.usage SETEVENTS => SETEVENTS [EXTENDED] [EVENTS]
-help.usage USEFEATURE => USEFEATURE OPTION
-help.usage SAVECONF => SAVECONF
-help.usage LOADCONF => LOADCONF...
-help.usage MAPADDRESS => MAPADDRESS SOURCE_ADDR=DESTINATION_ADDR
-help.usage POSTDESCRIPTOR => POSTDESCRIPTOR [purpose=general/controller/bridge] [cache=yes/no]...
-help.usage EXTENDCIRCUIT => EXTENDCIRCUIT CircuitID [PATH] [purpose=general/controller]
-help.usage SETCIRCUITPURPOSE => SETCIRCUITPURPOSE CircuitID purpose=general/controller
-help.usage CLOSECIRCUIT => CLOSECIRCUIT CircuitID [IfUnused]
-help.usage ATTACHSTREAM => ATTACHSTREAM StreamID CircuitID [HOP=HopNum]
-help.usage REDIRECTSTREAM => REDIRECTSTREAM StreamID Address [Port]
-help.usage CLOSESTREAM => CLOSESTREAM StreamID Reason [Flag]
-help.usage RESOLVE => RESOLVE [mode=reverse] address
-help.usage TAKEOWNERSHIP => TAKEOWNERSHIP
-help.usage PROTOCOLINFO => PROTOCOLINFO [ProtocolVersion]
-
-# Longer description of what tor and interpretor commands do.
-
-help.description.help
-|Provides usage information for the given interpretor, tor command, or tor
-|configuration option.
-|
-|Example:
-|  /help info        # provides a description of the '/info' option
-|  /help GETINFO     # usage information for tor's GETINFO controller option
-
-help.description.events
-|Provides events that we've received belonging to the given event types. If
-|no types are specified then this provides all the messages that we've
-|received.
-
-help.description.info
-|Provides general information for a relay that's currently in the consensus.
-|If no relay is specified then this provides information on ourselves.
-
-help.description.python
-|Enables or disables support for running python commands. This determines how
-|we treat commands this interpretor doesn't recognize...
-|
-|* If enabled then unrecognized commands are executed as python.
-|* If disabled then unrecognized commands are passed along to tor.
-
-help.description.quit
-|Terminates the interpretor.
-
-help.description.getinfo
-|Queries the tor process for information. Options are...
-|
-
-help.description.getconf
-|Provides the current value for a given configuration value. Options include...
-|
-
-help.description.setconf
-|Sets the given configuration parameters. Values can be quoted or non-quoted
-|strings, and reverts the option to 0 or NULL if not provided.
-|
-|Examples:
-|  * Sets a contact address and resets our family to NULL
-|    SETCONF MyFamily ContactInfo=foo at bar.com
-|
-|  * Sets an exit policy that only includes port 80/443
-|    SETCONF ExitPolicy=\"accept *:80, accept *:443, reject *:*\"\
-
-help.description.resetconf
-|Reverts the given configuration options to their default values. If a value
-|is provided then this behaves in the same way as SETCONF.
-|
-|Examples:
-|  * Returns both of our accounting parameters to their defaults
-|    RESETCONF AccountingMax AccountingStart
-|
-|  * Uses the default exit policy and sets our nickname to be 'Goomba'
-|    RESETCONF ExitPolicy Nickname=Goomba
-
-help.description.signal
-|Issues a signal that tells the tor process to reload its torrc, dump its
-|stats, halt, etc.
-
-help.description.setevents
-|Sets the events that we will receive. This turns off any events that aren't
-|listed so sending 'SETEVENTS' without any values will turn off all event reporting.
-|
-|For Tor versions between 0.1.1.9 and 0.2.2.1 adding 'EXTENDED' causes some
-|events to give us additional information. After version 0.2.2.1 this is
-|always on.
-|
-|Events include...
-|
-
-help.description.usefeature
-|Customizes the behavior of the control port. Options include...
-|
-
-help.description.saveconf
-|Writes Tor's current configuration to its torrc.
-
-help.description.loadconf
-|Reads the given text like it belonged to our torrc.
-|
-|Example:
-|  +LOADCONF
-|  # sets our exit policy to just accept ports 80 and 443
-|  ExitPolicy accept *:80
-|  ExitPolicy accept *:443
-|  ExitPolicy reject *:*
-|  .
-
-help.description.mapaddress
-|Replaces future requests for one address with another.
-|
-|Example:
-|  MAPADDRESS 0.0.0.0=torproject.org 1.2.3.4=tor.freehaven.net
-
-help.description.postdescriptor
-|Simulates getting a new relay descriptor.
-
-help.description.extendcircuit
-|Extends the given circuit or create a new one if the CircuitID is zero. The
-|PATH is a comma separated list of fingerprints. If it isn't set then this
-|uses Tor's normal path selection.
-
-help.description.setcircuitpurpose
-|Sets the purpose attribute for a circuit.
-
-help.description.closecircuit
-|Closes the given circuit. If "IfUnused" is included then this only closes
-|the circuit if it isn't currently being used.
-
-help.description.attachstream
-|Attaches a stream with the given built circuit (tor picks one on its own if
-|CircuitID is zero). If HopNum is given then this hop is used to exit the
-|circuit, otherwise the last relay is used.
-
-help.description.redirectstream
-|Sets the destination for a given stream. This can only be done after a
-|stream is created but before it's attached to a circuit.
-
-help.description.closestream
-|Closes the given stream, the reason being an integer matching a reason as
-|per section 6.3 of the tor-spec.
-
-help.description.resolve
-|Performs IPv4 DNS resolution over tor, doing a reverse lookup instead if
-|"mode=reverse" is included. This request is processed in the background and
-|results in a ADDRMAP event with the response.
-
-help.description.takeownership
-|Instructs Tor to gracefully shut down when this control connection is closed.
-
-help.description.protocolinfo
-|Provides bootstrapping information that a controller might need when first
-|starting, like Tor's version and controller authentication. This can be done
-|before authenticating to the control port.
-
-help.signal.options RELOAD / HUP => reload our torrc
-help.signal.options SHUTDOWN / INT => gracefully shut down, waiting 30 seconds if we're a relay
-help.signal.options DUMP / USR1 => logs information about open connections and circuits
-help.signal.options DEBUG / USR2 => makes us log at the DEBUG runlevel
-help.signal.options HALT / TERM => immediately shut down
-help.signal.options CLEARDNSCACHE => clears any cached DNS results
-help.signal.options NEWNYM => clears the DNS cache and uses new circuits for future connections
-
- ################
-# TAB COMPLETION #
- ################
-
-# Commands we'll autocomplete when the user hits tab. This is just the start of
-# our autocompletion list - more are determined dynamically by checking what
-# tor supports.
-
-autocomplete /help
-autocomplete /events
-autocomplete /info
-autocomplete /quit
-autocomplete SAVECONF
-autocomplete MAPADDRESS
-autocomplete EXTENDCIRCUIT
-autocomplete SETCIRCUITPURPOSE
-autocomplete SETROUTERPURPOSE
-autocomplete ATTACHSTREAM
-#autocomplete +POSTDESCRIPTOR  # TODO: needs multi-line support
-autocomplete REDIRECTSTREAM
-autocomplete CLOSESTREAM
-autocomplete CLOSECIRCUIT
-autocomplete QUIT
-autocomplete RESOLVE
-autocomplete PROTOCOLINFO
-#autocomplete +LOADCONF  # TODO: needs multi-line support
-autocomplete TAKEOWNERSHIP
-autocomplete AUTHCHALLENGE
-autocomplete DROPGUARDS
-
diff --git a/stem/util/system.py b/stem/util/system.py
index 7827eb6..c940578 100644
--- a/stem/util/system.py
+++ b/stem/util/system.py
@@ -910,7 +910,7 @@ def get_process_name():
       #
       #   ' '.join(['python'] + sys.argv)
       #
-      # ... doesn't do the trick since this will miss interpretor arguments.
+      # ... doesn't do the trick since this will miss interpreter arguments.
       #
       #   python -W ignore::DeprecationWarning my_script.py
 
diff --git a/test/settings.cfg b/test/settings.cfg
index 79852b0..2ef3736 100644
--- a/test/settings.cfg
+++ b/test/settings.cfg
@@ -183,10 +183,10 @@ test.unit_tests
 |test.unit.connection.authentication.TestAuthenticate
 |test.unit.connection.connect.TestConnect
 |test.unit.control.controller.TestControl
-|test.unit.interpretor.arguments.TestArgumentParsing
-|test.unit.interpretor.autocomplete.TestAutocompletion
-|test.unit.interpretor.help.TestHelpResponses
-|test.unit.interpretor.commands.TestInterpretorCommands
+|test.unit.interpreter.arguments.TestArgumentParsing
+|test.unit.interpreter.autocomplete.TestAutocompletion
+|test.unit.interpreter.help.TestHelpResponses
+|test.unit.interpreter.commands.TestInterpretorCommands
 |test.unit.doctest.TestDocumentation
 
 test.integ_tests
diff --git a/test/unit/interpreter/__init__.py b/test/unit/interpreter/__init__.py
new file mode 100644
index 0000000..7c10768
--- /dev/null
+++ b/test/unit/interpreter/__init__.py
@@ -0,0 +1,39 @@
+"""
+Unit tests for the stem's interpreter prompt.
+"""
+
+__all__ = [
+  'arguments',
+  'autocomplete',
+  'commands',
+  'help',
+]
+
+try:
+  # added in python 3.3
+  from unittest.mock import Mock
+except ImportError:
+  from mock import Mock
+
+GETINFO_NAMES = """
+info/names -- List of GETINFO options, types, and documentation.
+ip-to-country/* -- Perform a GEOIP lookup
+md/id/* -- Microdescriptors by ID
+""".strip()
+
+GETCONF_NAMES = """
+ExitNodes RouterList
+ExitPolicy LineList
+ExitPolicyRejectPrivate Boolean
+""".strip()
+
+
+CONTROLLER = Mock()
+
+CONTROLLER.get_info.side_effect = lambda arg, _: {
+  'info/names': GETINFO_NAMES,
+  'config/names': GETCONF_NAMES,
+  'events/names': 'BW DEBUG INFO NOTICE',
+  'features/names': 'VERBOSE_NAMES EXTENDED_EVENTS',
+  'signal/names': 'RELOAD HUP SHUTDOWN',
+}[arg]
diff --git a/test/unit/interpreter/arguments.py b/test/unit/interpreter/arguments.py
new file mode 100644
index 0000000..60fda3d
--- /dev/null
+++ b/test/unit/interpreter/arguments.py
@@ -0,0 +1,57 @@
+import unittest
+
+from stem.interpreter.arguments import DEFAULT_ARGS, parse, get_help
+
+
+class TestArgumentParsing(unittest.TestCase):
+  def test_that_we_get_default_values(self):
+    args = parse([])
+
+    for attr in DEFAULT_ARGS:
+      self.assertEqual(DEFAULT_ARGS[attr], getattr(args, attr))
+
+  def test_that_we_load_arguments(self):
+    args = parse(['--interface', '10.0.0.25:80'])
+    self.assertEqual('10.0.0.25', args.control_address)
+    self.assertEqual(80, args.control_port)
+
+    args = parse(['--interface', '80'])
+    self.assertEqual(DEFAULT_ARGS['control_address'], args.control_address)
+    self.assertEqual(80, args.control_port)
+
+    args = parse(['--socket', '/tmp/my_socket'])
+    self.assertEqual('/tmp/my_socket', args.control_socket)
+
+    args = parse(['--help'])
+    self.assertEqual(True, args.print_help)
+
+  def test_examples(self):
+    args = parse(['-i', '1643'])
+    self.assertEqual(1643, args.control_port)
+
+    args = parse(['-s', '~/.tor/socket'])
+    self.assertEqual('~/.tor/socket', args.control_socket)
+
+  def test_that_we_reject_unrecognized_arguments(self):
+    self.assertRaises(ValueError, parse, ['--blarg', 'stuff'])
+
+  def test_that_we_reject_invalid_interfaces(self):
+    invalid_inputs = (
+      '',
+      '    ',
+      'blarg',
+      '127.0.0.1',
+      '127.0.0.1:',
+      ':80',
+      '400.0.0.1:80',
+      '127.0.0.1:-5',
+      '127.0.0.1:500000',
+    )
+
+    for invalid_input in invalid_inputs:
+      self.assertRaises(ValueError, parse, ['--interface', invalid_input])
+
+  def test_get_help(self):
+    help_text = get_help()
+    self.assertTrue('Interactive interpreter for Tor.' in help_text)
+    self.assertTrue('change control interface from 127.0.0.1:9051' in help_text)
diff --git a/test/unit/interpreter/autocomplete.py b/test/unit/interpreter/autocomplete.py
new file mode 100644
index 0000000..40bcab4
--- /dev/null
+++ b/test/unit/interpreter/autocomplete.py
@@ -0,0 +1,112 @@
+import unittest
+
+from stem.interpreter.autocomplete import _get_commands, Autocompleter
+
+from test.unit.interpreter import CONTROLLER
+
+try:
+  # added in python 3.3
+  from unittest.mock import Mock
+except ImportError:
+  from mock import Mock
+
+
+class TestAutocompletion(unittest.TestCase):
+  def test_autocomplete_results_from_config(self):
+    """
+    Check that we load autocompletion results from our configuration.
+    """
+
+    commands = _get_commands(None)
+    self.assertTrue('PROTOCOLINFO' in commands)
+    self.assertTrue('/quit' in commands)
+
+  def test_autocomplete_results_from_tor(self):
+    """
+    Check our ability to determine autocompletion results based on our tor
+    instance's capabilities.
+    """
+
+    # Check that when GETINFO requests fail we have base commands, but nothing
+    # with arguments.
+
+    controller = Mock()
+    controller.get_info.return_value = None
+    commands = _get_commands(controller)
+
+    self.assertTrue('GETINFO ' in commands)
+    self.assertTrue('GETCONF ' in commands)
+    self.assertTrue('SIGNAL ' in commands)
+
+    self.assertFalse('GETINFO info/names' in commands)
+    self.assertFalse('GETCONF ExitPolicy' in commands)
+    self.assertFalse('SIGNAL SHUTDOWN' in commands)
+
+    # Now check where we should be able to determine tor's capabilities.
+
+    commands = _get_commands(CONTROLLER)
+
+    expected = (
+      'GETINFO info/names',
+      'GETINFO ip-to-country/',
+      'GETINFO md/id/',
+
+      'GETCONF ExitNodes',
+      'GETCONF ExitPolicy',
+      'SETCONF ExitPolicy',
+      'RESETCONF ExitPolicy',
+
+      'SETEVENTS BW',
+      'SETEVENTS INFO',
+      'USEFEATURE VERBOSE_NAMES',
+      'USEFEATURE EXTENDED_EVENTS',
+      'SIGNAL RELOAD',
+      'SIGNAL SHUTDOWN',
+    )
+
+    for result in expected:
+      self.assertTrue(result in commands)
+
+    # We shouldn't include the base commands since we have results with
+    # their arguments.
+
+    self.assertFalse('GETINFO ' in commands)
+    self.assertFalse('GETCONF ' in commands)
+    self.assertFalse('SIGNAL ' in commands)
+
+  def test_autocompleter_match(self):
+    """
+    Exercise our Autocompleter's match method.
+    """
+
+    autocompleter = Autocompleter(None)
+
+    self.assertEqual(['/help'], autocompleter.matches('/help'))
+    self.assertEqual(['/help'], autocompleter.matches('/hel'))
+    self.assertEqual(['/help'], autocompleter.matches('/he'))
+    self.assertEqual(['/help'], autocompleter.matches('/h'))
+    self.assertEqual(['/help', '/events', '/info', '/quit'], autocompleter.matches('/'))
+
+    # check case sensitivity
+
+    self.assertEqual(['/help'], autocompleter.matches('/HELP'))
+    self.assertEqual(['/help'], autocompleter.matches('/HeLp'))
+
+    # check when we shouldn't have any matches
+
+    self.assertEqual([], autocompleter.matches('blarg'))
+
+  def test_autocompleter_complete(self):
+    """
+    Exercise our Autocompleter's complete method.
+    """
+
+    autocompleter = Autocompleter(None)
+
+    self.assertEqual('/help', autocompleter.complete('/', 0))
+    self.assertEqual('/events', autocompleter.complete('/', 1))
+    self.assertEqual('/info', autocompleter.complete('/', 2))
+    self.assertEqual('/quit', autocompleter.complete('/', 3))
+    self.assertEqual(None, autocompleter.complete('/', 4))
+
+    self.assertEqual(None, autocompleter.complete('blarg', 0))
diff --git a/test/unit/interpreter/commands.py b/test/unit/interpreter/commands.py
new file mode 100644
index 0000000..9afab59
--- /dev/null
+++ b/test/unit/interpreter/commands.py
@@ -0,0 +1,198 @@
+import datetime
+import unittest
+
+import stem
+import stem.response
+import stem.version
+
+from stem.interpreter.commands import ControlInterpretor, _get_fingerprint
+
+from test import mocking
+from test.unit.interpreter import CONTROLLER
+
+try:
+  # added in python 3.3
+  from unittest.mock import Mock
+except ImportError:
+  from mock import Mock
+
+EXPECTED_EVENTS_RESPONSE = """\
+\x1b[34mBW 15 25\x1b[0m
+\x1b[34mBW 758 570\x1b[0m
+\x1b[34mDEBUG connection_edge_process_relay_cell(): Got an extended cell! Yay.\x1b[0m
+"""
+
+EXPECTED_INFO_RESPONSE = """\
+moria1 (9695DFC35FFEB861329B9F1AB04C46397020CE31)
+\x1b[34;1maddress: \x1b[0m128.31.0.34:9101 (us)
+\x1b[34;1mpublished: \x1b[0m05:52:05 05/05/2014
+\x1b[34;1mos: \x1b[0mLinux
+\x1b[34;1mversion: \x1b[0m0.2.5.3-alpha-dev
+\x1b[34;1mflags: \x1b[0mAuthority, Fast, Guard, HSDir, Named, Running, Stable, V2Dir, Valid
+\x1b[34;1mexit policy: \x1b[0mreject 1-65535
+\x1b[34;1mcontact: \x1b[0m1024D/28988BF5 arma mit edu
+"""
+
+EXPECTED_GETCONF_RESPONSE = """\
+\x1b[34;1mlog\x1b[0m\x1b[34m => notice stdout\x1b[0m
+\x1b[34;1maddress\x1b[0m\x1b[34m => \x1b[0m
+
+"""
+
+FINGERPRINT = '9695DFC35FFEB861329B9F1AB04C46397020CE31'
+
+
+class TestInterpretorCommands(unittest.TestCase):
+  def test_get_fingerprint_for_ourselves(self):
+    controller = Mock()
+
+    controller.get_info.side_effect = lambda arg: {
+      'fingerprint': FINGERPRINT,
+    }[arg]
+
+    self.assertEqual(FINGERPRINT, _get_fingerprint('', controller))
+
+    controller.get_info.side_effect = stem.ControllerError
+    self.assertRaises(ValueError, _get_fingerprint, '', controller)
+
+  def test_get_fingerprint_for_fingerprint(self):
+    self.assertEqual(FINGERPRINT, _get_fingerprint(FINGERPRINT, Mock()))
+
+  def test_get_fingerprint_for_nickname(self):
+    controller, descriptor = Mock(), Mock()
+    descriptor.fingerprint = FINGERPRINT
+
+    controller.get_network_status.side_effect = lambda arg: {
+      'moria1': descriptor,
+    }[arg]
+
+    self.assertEqual(FINGERPRINT, _get_fingerprint('moria1', controller))
+
+    controller.get_network_status.side_effect = stem.ControllerError
+    self.assertRaises(ValueError, _get_fingerprint, 'moria1', controller)
+
+  def test_get_fingerprint_for_address(self):
+    controller = Mock()
+
+    self.assertRaises(ValueError, _get_fingerprint, '127.0.0.1:-1', controller)
+    self.assertRaises(ValueError, _get_fingerprint, '127.0.0.901:80', controller)
+
+    descriptor = Mock()
+    descriptor.address = '127.0.0.1'
+    descriptor.or_port = 80
+    descriptor.fingerprint = FINGERPRINT
+
+    controller.get_network_statuses.return_value = [descriptor]
+
+    self.assertEqual(FINGERPRINT, _get_fingerprint('127.0.0.1', controller))
+    self.assertEqual(FINGERPRINT, _get_fingerprint('127.0.0.1:80', controller))
+    self.assertRaises(ValueError, _get_fingerprint, '127.0.0.1:81', controller)
+    self.assertRaises(ValueError, _get_fingerprint, '127.0.0.2', controller)
+
+  def test_get_fingerprint_for_unrecognized_inputs(self):
+    self.assertRaises(ValueError, _get_fingerprint, 'blarg!', Mock())
+
+  def test_when_disconnected(self):
+    controller = Mock()
+    controller.is_alive.return_value = False
+
+    interpreter = ControlInterpretor(controller)
+    self.assertRaises(stem.SocketClosed, interpreter.run_command, '/help')
+
+  def test_quit(self):
+    interpreter = ControlInterpretor(CONTROLLER)
+    self.assertRaises(stem.SocketClosed, interpreter.run_command, '/quit')
+    self.assertRaises(stem.SocketClosed, interpreter.run_command, 'QUIT')
+
+  def test_help(self):
+    interpreter = ControlInterpretor(CONTROLLER)
+
+    self.assertTrue('Interpretor commands include:' in interpreter.run_command('/help'))
+    self.assertTrue('Queries the tor process for information.' in interpreter.run_command('/help GETINFO'))
+    self.assertTrue('Queries the tor process for information.' in interpreter.run_command('/help GETINFO version'))
+
+  def test_events(self):
+    interpreter = ControlInterpretor(CONTROLLER)
+
+    # no received events
+
+    self.assertEqual('\n', interpreter.run_command('/events'))
+
+    # with enqueued events
+
+    event_contents = (
+      '650 BW 15 25',
+      '650 BW 758 570',
+      '650 DEBUG connection_edge_process_relay_cell(): Got an extended cell! Yay.',
+    )
+
+    for content in event_contents:
+      event = mocking.get_message(content)
+      stem.response.convert('EVENT', event)
+      interpreter._received_events.append(event)
+
+    self.assertEqual(EXPECTED_EVENTS_RESPONSE, interpreter.run_command('/events'))
+
+  def test_info(self):
+    controller, server_desc, ns_desc = Mock(), Mock(), Mock()
+
+    controller.get_microdescriptor.return_value = None
+    controller.get_server_descriptor.return_value = server_desc
+    controller.get_network_status.return_value = ns_desc
+
+    controller.get_info.side_effect = lambda arg, _: {
+      'ip-to-country/128.31.0.34': 'us',
+    }[arg]
+
+    ns_desc.address = '128.31.0.34'
+    ns_desc.or_port = 9101
+    ns_desc.published = datetime.datetime(2014, 5, 5, 5, 52, 5)
+    ns_desc.nickname = 'moria1'
+    ns_desc.flags = ['Authority', 'Fast', 'Guard', 'HSDir', 'Named', 'Running', 'Stable', 'V2Dir', 'Valid']
+
+    server_desc.exit_policy.summary.return_value = 'reject 1-65535'
+    server_desc.platform = 'Linux'
+    server_desc.tor_version = stem.version.Version('0.2.5.3-alpha-dev')
+    server_desc.contact = '1024D/28988BF5 arma mit edu'
+
+    interpreter = ControlInterpretor(controller)
+    self.assertEqual(EXPECTED_INFO_RESPONSE, interpreter.run_command('/info ' + FINGERPRINT))
+
+  def test_unrecognized_interpreter_command(self):
+    interpreter = ControlInterpretor(CONTROLLER)
+
+    expected = "\x1b[1;31m'/unrecognized' isn't a recognized command\x1b[0m\n"
+    self.assertEqual(expected, interpreter.run_command('/unrecognized'))
+
+  def test_getinfo(self):
+    response = '250-version=0.2.5.1-alpha-dev (git-245ecfff36c0cecc)\r\n250 OK'
+
+    controller = Mock()
+    controller.msg.return_value = mocking.get_message(response)
+
+    interpreter = ControlInterpretor(controller)
+
+    self.assertEqual('\x1b[34m%s\x1b[0m\n' % response, interpreter.run_command('GETINFO version'))
+    controller.msg.assert_called_with('GETINFO version')
+
+    controller.msg.side_effect = stem.ControllerError('kaboom!')
+    self.assertEqual('\x1b[1;31mkaboom!\x1b[0m\n', interpreter.run_command('getinfo process/user'))
+
+  def test_getconf(self):
+    response = '250-Log=notice stdout\r\n250 Address'
+
+    controller = Mock()
+    controller.msg.return_value = mocking.get_message(response)
+
+    interpreter = ControlInterpretor(controller)
+
+    self.assertEqual('\x1b[34m%s\x1b[0m\n' % response, interpreter.run_command('GETCONF log address'))
+    controller.msg.assert_called_with('GETCONF log address')
+
+  def test_setevents(self):
+    controller = Mock()
+    controller.msg.return_value = mocking.get_message('250 OK')
+
+    interpreter = ControlInterpretor(controller)
+
+    self.assertEqual('\x1b[34m250 OK\x1b[0m\n', interpreter.run_command('SETEVENTS BW'))
diff --git a/test/unit/interpreter/help.py b/test/unit/interpreter/help.py
new file mode 100644
index 0000000..49df746
--- /dev/null
+++ b/test/unit/interpreter/help.py
@@ -0,0 +1,54 @@
+import unittest
+
+from stem.interpreter.help import response, _normalize
+
+from test.unit.interpreter import CONTROLLER
+
+
+class TestHelpResponses(unittest.TestCase):
+  def test_normalization(self):
+    self.assertEqual('', _normalize(''))
+    self.assertEqual('', _normalize('   '))
+
+    self.assertEqual('GETINFO', _normalize('GETINFO'))
+    self.assertEqual('GETINFO', _normalize('GetInfo'))
+    self.assertEqual('GETINFO', _normalize('getinfo'))
+    self.assertEqual('GETINFO', _normalize('GETINFO version'))
+    self.assertEqual('GETINFO', _normalize('GETINFO   '))
+
+    self.assertEqual('INFO', _normalize('/info'))
+    self.assertEqual('INFO', _normalize('/info caerSidi'))
+
+  def test_unrecognized_option(self):
+    result = response(CONTROLLER, 'FOOBAR')
+    self.assertEqual("\x1b[1;31mNo help information available for 'FOOBAR'...\x1b[0m", result)
+
+  def test_general_help(self):
+    result = response(CONTROLLER, '')
+    self.assertTrue('Interpretor commands include:' in result)
+    self.assertTrue('\x1b[34;1m  GETINFO\x1b[0m\x1b[34m - queries information from tor\x1b[0m\n' in result)
+
+  def test_getinfo_help(self):
+    result = response(CONTROLLER, 'GETINFO')
+    self.assertTrue('Queries the tor process for information. Options are...' in result)
+    self.assertTrue('\x1b[34;1minfo/names                       \x1b[0m\x1b[34m - List of GETINFO options, types, and documentation.' in result)
+
+  def test_getconf_help(self):
+    result = response(CONTROLLER, 'GETCONF')
+    self.assertTrue('Provides the current value for a given configuration value. Options include...' in result)
+    self.assertTrue('\x1b[34mExitNodes                                 ExitPolicy' in result)
+
+  def test_signal_help(self):
+    result = response(CONTROLLER, 'SIGNAL')
+    self.assertTrue('Issues a signal that tells the tor process to' in result)
+    self.assertTrue('\x1b[34;1mRELOAD / HUP   \x1b[0m\x1b[34m - reload our torrc' in result)
+
+  def test_setevents_help(self):
+    result = response(CONTROLLER, 'SETEVENTS')
+    self.assertTrue('Sets the events that we will receive.' in result)
+    self.assertTrue('\x1b[34mBW                  DEBUG               INFO                NOTICE\x1b[0m' in result)
+
+  def test_usefeature_help(self):
+    result = response(CONTROLLER, 'USEFEATURE')
+    self.assertTrue('Customizes the behavior of the control port.' in result)
+    self.assertTrue('\x1b[34mVERBOSE_NAMES EXTENDED_EVENTS\x1b[0m' in result)
diff --git a/test/unit/interpretor/__init__.py b/test/unit/interpretor/__init__.py
deleted file mode 100644
index 705734d..0000000
--- a/test/unit/interpretor/__init__.py
+++ /dev/null
@@ -1,39 +0,0 @@
-"""
-Unit tests for the stem's interpretor prompt.
-"""
-
-__all__ = [
-  'arguments',
-  'autocomplete',
-  'commands',
-  'help',
-]
-
-try:
-  # added in python 3.3
-  from unittest.mock import Mock
-except ImportError:
-  from mock import Mock
-
-GETINFO_NAMES = """
-info/names -- List of GETINFO options, types, and documentation.
-ip-to-country/* -- Perform a GEOIP lookup
-md/id/* -- Microdescriptors by ID
-""".strip()
-
-GETCONF_NAMES = """
-ExitNodes RouterList
-ExitPolicy LineList
-ExitPolicyRejectPrivate Boolean
-""".strip()
-
-
-CONTROLLER = Mock()
-
-CONTROLLER.get_info.side_effect = lambda arg, _: {
-  'info/names': GETINFO_NAMES,
-  'config/names': GETCONF_NAMES,
-  'events/names': 'BW DEBUG INFO NOTICE',
-  'features/names': 'VERBOSE_NAMES EXTENDED_EVENTS',
-  'signal/names': 'RELOAD HUP SHUTDOWN',
-}[arg]
diff --git a/test/unit/interpretor/arguments.py b/test/unit/interpretor/arguments.py
deleted file mode 100644
index ab835c4..0000000
--- a/test/unit/interpretor/arguments.py
+++ /dev/null
@@ -1,57 +0,0 @@
-import unittest
-
-from stem.interpretor.arguments import DEFAULT_ARGS, parse, get_help
-
-
-class TestArgumentParsing(unittest.TestCase):
-  def test_that_we_get_default_values(self):
-    args = parse([])
-
-    for attr in DEFAULT_ARGS:
-      self.assertEqual(DEFAULT_ARGS[attr], getattr(args, attr))
-
-  def test_that_we_load_arguments(self):
-    args = parse(['--interface', '10.0.0.25:80'])
-    self.assertEqual('10.0.0.25', args.control_address)
-    self.assertEqual(80, args.control_port)
-
-    args = parse(['--interface', '80'])
-    self.assertEqual(DEFAULT_ARGS['control_address'], args.control_address)
-    self.assertEqual(80, args.control_port)
-
-    args = parse(['--socket', '/tmp/my_socket'])
-    self.assertEqual('/tmp/my_socket', args.control_socket)
-
-    args = parse(['--help'])
-    self.assertEqual(True, args.print_help)
-
-  def test_examples(self):
-    args = parse(['-i', '1643'])
-    self.assertEqual(1643, args.control_port)
-
-    args = parse(['-s', '~/.tor/socket'])
-    self.assertEqual('~/.tor/socket', args.control_socket)
-
-  def test_that_we_reject_unrecognized_arguments(self):
-    self.assertRaises(ValueError, parse, ['--blarg', 'stuff'])
-
-  def test_that_we_reject_invalid_interfaces(self):
-    invalid_inputs = (
-      '',
-      '    ',
-      'blarg',
-      '127.0.0.1',
-      '127.0.0.1:',
-      ':80',
-      '400.0.0.1:80',
-      '127.0.0.1:-5',
-      '127.0.0.1:500000',
-    )
-
-    for invalid_input in invalid_inputs:
-      self.assertRaises(ValueError, parse, ['--interface', invalid_input])
-
-  def test_get_help(self):
-    help_text = get_help()
-    self.assertTrue('Interactive interpretor for Tor.' in help_text)
-    self.assertTrue('change control interface from 127.0.0.1:9051' in help_text)
diff --git a/test/unit/interpretor/autocomplete.py b/test/unit/interpretor/autocomplete.py
deleted file mode 100644
index 6541da3..0000000
--- a/test/unit/interpretor/autocomplete.py
+++ /dev/null
@@ -1,112 +0,0 @@
-import unittest
-
-from stem.interpretor.autocomplete import _get_commands, Autocompleter
-
-from test.unit.interpretor import CONTROLLER
-
-try:
-  # added in python 3.3
-  from unittest.mock import Mock
-except ImportError:
-  from mock import Mock
-
-
-class TestAutocompletion(unittest.TestCase):
-  def test_autocomplete_results_from_config(self):
-    """
-    Check that we load autocompletion results from our configuration.
-    """
-
-    commands = _get_commands(None)
-    self.assertTrue('PROTOCOLINFO' in commands)
-    self.assertTrue('/quit' in commands)
-
-  def test_autocomplete_results_from_tor(self):
-    """
-    Check our ability to determine autocompletion results based on our tor
-    instance's capabilities.
-    """
-
-    # Check that when GETINFO requests fail we have base commands, but nothing
-    # with arguments.
-
-    controller = Mock()
-    controller.get_info.return_value = None
-    commands = _get_commands(controller)
-
-    self.assertTrue('GETINFO ' in commands)
-    self.assertTrue('GETCONF ' in commands)
-    self.assertTrue('SIGNAL ' in commands)
-
-    self.assertFalse('GETINFO info/names' in commands)
-    self.assertFalse('GETCONF ExitPolicy' in commands)
-    self.assertFalse('SIGNAL SHUTDOWN' in commands)
-
-    # Now check where we should be able to determine tor's capabilities.
-
-    commands = _get_commands(CONTROLLER)
-
-    expected = (
-      'GETINFO info/names',
-      'GETINFO ip-to-country/',
-      'GETINFO md/id/',
-
-      'GETCONF ExitNodes',
-      'GETCONF ExitPolicy',
-      'SETCONF ExitPolicy',
-      'RESETCONF ExitPolicy',
-
-      'SETEVENTS BW',
-      'SETEVENTS INFO',
-      'USEFEATURE VERBOSE_NAMES',
-      'USEFEATURE EXTENDED_EVENTS',
-      'SIGNAL RELOAD',
-      'SIGNAL SHUTDOWN',
-    )
-
-    for result in expected:
-      self.assertTrue(result in commands)
-
-    # We shouldn't include the base commands since we have results with
-    # their arguments.
-
-    self.assertFalse('GETINFO ' in commands)
-    self.assertFalse('GETCONF ' in commands)
-    self.assertFalse('SIGNAL ' in commands)
-
-  def test_autocompleter_match(self):
-    """
-    Exercise our Autocompleter's match method.
-    """
-
-    autocompleter = Autocompleter(None)
-
-    self.assertEqual(['/help'], autocompleter.matches('/help'))
-    self.assertEqual(['/help'], autocompleter.matches('/hel'))
-    self.assertEqual(['/help'], autocompleter.matches('/he'))
-    self.assertEqual(['/help'], autocompleter.matches('/h'))
-    self.assertEqual(['/help', '/events', '/info', '/quit'], autocompleter.matches('/'))
-
-    # check case sensitivity
-
-    self.assertEqual(['/help'], autocompleter.matches('/HELP'))
-    self.assertEqual(['/help'], autocompleter.matches('/HeLp'))
-
-    # check when we shouldn't have any matches
-
-    self.assertEqual([], autocompleter.matches('blarg'))
-
-  def test_autocompleter_complete(self):
-    """
-    Exercise our Autocompleter's complete method.
-    """
-
-    autocompleter = Autocompleter(None)
-
-    self.assertEqual('/help', autocompleter.complete('/', 0))
-    self.assertEqual('/events', autocompleter.complete('/', 1))
-    self.assertEqual('/info', autocompleter.complete('/', 2))
-    self.assertEqual('/quit', autocompleter.complete('/', 3))
-    self.assertEqual(None, autocompleter.complete('/', 4))
-
-    self.assertEqual(None, autocompleter.complete('blarg', 0))
diff --git a/test/unit/interpretor/commands.py b/test/unit/interpretor/commands.py
deleted file mode 100644
index dfdbddb..0000000
--- a/test/unit/interpretor/commands.py
+++ /dev/null
@@ -1,198 +0,0 @@
-import datetime
-import unittest
-
-import stem
-import stem.response
-import stem.version
-
-from stem.interpretor.commands import ControlInterpretor, _get_fingerprint
-
-from test import mocking
-from test.unit.interpretor import CONTROLLER
-
-try:
-  # added in python 3.3
-  from unittest.mock import Mock
-except ImportError:
-  from mock import Mock
-
-EXPECTED_EVENTS_RESPONSE = """\
-\x1b[34mBW 15 25\x1b[0m
-\x1b[34mBW 758 570\x1b[0m
-\x1b[34mDEBUG connection_edge_process_relay_cell(): Got an extended cell! Yay.\x1b[0m
-"""
-
-EXPECTED_INFO_RESPONSE = """\
-moria1 (9695DFC35FFEB861329B9F1AB04C46397020CE31)
-\x1b[34;1maddress: \x1b[0m128.31.0.34:9101 (us)
-\x1b[34;1mpublished: \x1b[0m05:52:05 05/05/2014
-\x1b[34;1mos: \x1b[0mLinux
-\x1b[34;1mversion: \x1b[0m0.2.5.3-alpha-dev
-\x1b[34;1mflags: \x1b[0mAuthority, Fast, Guard, HSDir, Named, Running, Stable, V2Dir, Valid
-\x1b[34;1mexit policy: \x1b[0mreject 1-65535
-\x1b[34;1mcontact: \x1b[0m1024D/28988BF5 arma mit edu
-"""
-
-EXPECTED_GETCONF_RESPONSE = """\
-\x1b[34;1mlog\x1b[0m\x1b[34m => notice stdout\x1b[0m
-\x1b[34;1maddress\x1b[0m\x1b[34m => \x1b[0m
-
-"""
-
-FINGERPRINT = '9695DFC35FFEB861329B9F1AB04C46397020CE31'
-
-
-class TestInterpretorCommands(unittest.TestCase):
-  def test_get_fingerprint_for_ourselves(self):
-    controller = Mock()
-
-    controller.get_info.side_effect = lambda arg: {
-      'fingerprint': FINGERPRINT,
-    }[arg]
-
-    self.assertEqual(FINGERPRINT, _get_fingerprint('', controller))
-
-    controller.get_info.side_effect = stem.ControllerError
-    self.assertRaises(ValueError, _get_fingerprint, '', controller)
-
-  def test_get_fingerprint_for_fingerprint(self):
-    self.assertEqual(FINGERPRINT, _get_fingerprint(FINGERPRINT, Mock()))
-
-  def test_get_fingerprint_for_nickname(self):
-    controller, descriptor = Mock(), Mock()
-    descriptor.fingerprint = FINGERPRINT
-
-    controller.get_network_status.side_effect = lambda arg: {
-      'moria1': descriptor,
-    }[arg]
-
-    self.assertEqual(FINGERPRINT, _get_fingerprint('moria1', controller))
-
-    controller.get_network_status.side_effect = stem.ControllerError
-    self.assertRaises(ValueError, _get_fingerprint, 'moria1', controller)
-
-  def test_get_fingerprint_for_address(self):
-    controller = Mock()
-
-    self.assertRaises(ValueError, _get_fingerprint, '127.0.0.1:-1', controller)
-    self.assertRaises(ValueError, _get_fingerprint, '127.0.0.901:80', controller)
-
-    descriptor = Mock()
-    descriptor.address = '127.0.0.1'
-    descriptor.or_port = 80
-    descriptor.fingerprint = FINGERPRINT
-
-    controller.get_network_statuses.return_value = [descriptor]
-
-    self.assertEqual(FINGERPRINT, _get_fingerprint('127.0.0.1', controller))
-    self.assertEqual(FINGERPRINT, _get_fingerprint('127.0.0.1:80', controller))
-    self.assertRaises(ValueError, _get_fingerprint, '127.0.0.1:81', controller)
-    self.assertRaises(ValueError, _get_fingerprint, '127.0.0.2', controller)
-
-  def test_get_fingerprint_for_unrecognized_inputs(self):
-    self.assertRaises(ValueError, _get_fingerprint, 'blarg!', Mock())
-
-  def test_when_disconnected(self):
-    controller = Mock()
-    controller.is_alive.return_value = False
-
-    interpretor = ControlInterpretor(controller)
-    self.assertRaises(stem.SocketClosed, interpretor.run_command, '/help')
-
-  def test_quit(self):
-    interpretor = ControlInterpretor(CONTROLLER)
-    self.assertRaises(stem.SocketClosed, interpretor.run_command, '/quit')
-    self.assertRaises(stem.SocketClosed, interpretor.run_command, 'QUIT')
-
-  def test_help(self):
-    interpretor = ControlInterpretor(CONTROLLER)
-
-    self.assertTrue('Interpretor commands include:' in interpretor.run_command('/help'))
-    self.assertTrue('Queries the tor process for information.' in interpretor.run_command('/help GETINFO'))
-    self.assertTrue('Queries the tor process for information.' in interpretor.run_command('/help GETINFO version'))
-
-  def test_events(self):
-    interpretor = ControlInterpretor(CONTROLLER)
-
-    # no received events
-
-    self.assertEqual('\n', interpretor.run_command('/events'))
-
-    # with enqueued events
-
-    event_contents = (
-      '650 BW 15 25',
-      '650 BW 758 570',
-      '650 DEBUG connection_edge_process_relay_cell(): Got an extended cell! Yay.',
-    )
-
-    for content in event_contents:
-      event = mocking.get_message(content)
-      stem.response.convert('EVENT', event)
-      interpretor._received_events.append(event)
-
-    self.assertEqual(EXPECTED_EVENTS_RESPONSE, interpretor.run_command('/events'))
-
-  def test_info(self):
-    controller, server_desc, ns_desc = Mock(), Mock(), Mock()
-
-    controller.get_microdescriptor.return_value = None
-    controller.get_server_descriptor.return_value = server_desc
-    controller.get_network_status.return_value = ns_desc
-
-    controller.get_info.side_effect = lambda arg, _: {
-      'ip-to-country/128.31.0.34': 'us',
-    }[arg]
-
-    ns_desc.address = '128.31.0.34'
-    ns_desc.or_port = 9101
-    ns_desc.published = datetime.datetime(2014, 5, 5, 5, 52, 5)
-    ns_desc.nickname = 'moria1'
-    ns_desc.flags = ['Authority', 'Fast', 'Guard', 'HSDir', 'Named', 'Running', 'Stable', 'V2Dir', 'Valid']
-
-    server_desc.exit_policy.summary.return_value = 'reject 1-65535'
-    server_desc.platform = 'Linux'
-    server_desc.tor_version = stem.version.Version('0.2.5.3-alpha-dev')
-    server_desc.contact = '1024D/28988BF5 arma mit edu'
-
-    interpretor = ControlInterpretor(controller)
-    self.assertEqual(EXPECTED_INFO_RESPONSE, interpretor.run_command('/info ' + FINGERPRINT))
-
-  def test_unrecognized_interpretor_command(self):
-    interpretor = ControlInterpretor(CONTROLLER)
-
-    expected = "\x1b[1;31m'/unrecognized' isn't a recognized command\x1b[0m\n"
-    self.assertEqual(expected, interpretor.run_command('/unrecognized'))
-
-  def test_getinfo(self):
-    response = '250-version=0.2.5.1-alpha-dev (git-245ecfff36c0cecc)\r\n250 OK'
-
-    controller = Mock()
-    controller.msg.return_value = mocking.get_message(response)
-
-    interpretor = ControlInterpretor(controller)
-
-    self.assertEqual('\x1b[34m%s\x1b[0m\n' % response, interpretor.run_command('GETINFO version'))
-    controller.msg.assert_called_with('GETINFO version')
-
-    controller.msg.side_effect = stem.ControllerError('kaboom!')
-    self.assertEqual('\x1b[1;31mkaboom!\x1b[0m\n', interpretor.run_command('getinfo process/user'))
-
-  def test_getconf(self):
-    response = '250-Log=notice stdout\r\n250 Address'
-
-    controller = Mock()
-    controller.msg.return_value = mocking.get_message(response)
-
-    interpretor = ControlInterpretor(controller)
-
-    self.assertEqual('\x1b[34m%s\x1b[0m\n' % response, interpretor.run_command('GETCONF log address'))
-    controller.msg.assert_called_with('GETCONF log address')
-
-  def test_setevents(self):
-    controller = Mock()
-    controller.msg.return_value = mocking.get_message('250 OK')
-
-    interpretor = ControlInterpretor(controller)
-
-    self.assertEqual('\x1b[34m250 OK\x1b[0m\n', interpretor.run_command('SETEVENTS BW'))
diff --git a/test/unit/interpretor/help.py b/test/unit/interpretor/help.py
deleted file mode 100644
index e656060..0000000
--- a/test/unit/interpretor/help.py
+++ /dev/null
@@ -1,54 +0,0 @@
-import unittest
-
-from stem.interpretor.help import response, _normalize
-
-from test.unit.interpretor import CONTROLLER
-
-
-class TestHelpResponses(unittest.TestCase):
-  def test_normalization(self):
-    self.assertEqual('', _normalize(''))
-    self.assertEqual('', _normalize('   '))
-
-    self.assertEqual('GETINFO', _normalize('GETINFO'))
-    self.assertEqual('GETINFO', _normalize('GetInfo'))
-    self.assertEqual('GETINFO', _normalize('getinfo'))
-    self.assertEqual('GETINFO', _normalize('GETINFO version'))
-    self.assertEqual('GETINFO', _normalize('GETINFO   '))
-
-    self.assertEqual('INFO', _normalize('/info'))
-    self.assertEqual('INFO', _normalize('/info caerSidi'))
-
-  def test_unrecognized_option(self):
-    result = response(CONTROLLER, 'FOOBAR')
-    self.assertEqual("\x1b[1;31mNo help information available for 'FOOBAR'...\x1b[0m", result)
-
-  def test_general_help(self):
-    result = response(CONTROLLER, '')
-    self.assertTrue('Interpretor commands include:' in result)
-    self.assertTrue('\x1b[34;1m  GETINFO\x1b[0m\x1b[34m - queries information from tor\x1b[0m\n' in result)
-
-  def test_getinfo_help(self):
-    result = response(CONTROLLER, 'GETINFO')
-    self.assertTrue('Queries the tor process for information. Options are...' in result)
-    self.assertTrue('\x1b[34;1minfo/names                       \x1b[0m\x1b[34m - List of GETINFO options, types, and documentation.' in result)
-
-  def test_getconf_help(self):
-    result = response(CONTROLLER, 'GETCONF')
-    self.assertTrue('Provides the current value for a given configuration value. Options include...' in result)
-    self.assertTrue('\x1b[34mExitNodes                                 ExitPolicy' in result)
-
-  def test_signal_help(self):
-    result = response(CONTROLLER, 'SIGNAL')
-    self.assertTrue('Issues a signal that tells the tor process to' in result)
-    self.assertTrue('\x1b[34;1mRELOAD / HUP   \x1b[0m\x1b[34m - reload our torrc' in result)
-
-  def test_setevents_help(self):
-    result = response(CONTROLLER, 'SETEVENTS')
-    self.assertTrue('Sets the events that we will receive.' in result)
-    self.assertTrue('\x1b[34mBW                  DEBUG               INFO                NOTICE\x1b[0m' in result)
-
-  def test_usefeature_help(self):
-    result = response(CONTROLLER, 'USEFEATURE')
-    self.assertTrue('Customizes the behavior of the control port.' in result)
-    self.assertTrue('\x1b[34mVERBOSE_NAMES EXTENDED_EVENTS\x1b[0m' in result)
diff --git a/tor-prompt b/tor-prompt
index fc115a3..0f3755b 100755
--- a/tor-prompt
+++ b/tor-prompt
@@ -2,7 +2,7 @@
 # Copyright 2014, Damian Johnson and The Tor Project
 # See LICENSE for licensing information
 
-import stem.interpretor
+import stem.interpreter
 
 if __name__ == '__main__':
-  stem.interpretor.main()
+  stem.interpreter.main()





More information about the tor-commits mailing list