commit d14fc24d02bd73c45963d0a6edfabdb93812b0a8 Author: Damian Johnson atagar@torproject.org Date: Mon Apr 17 12:49:06 2017 -0700
tor-prompt --run argument for running commands
Neat idea from adrelanos on...
https://trac.torproject.org/projects/tor/ticket/21541
Adding a --run argument that can either be used to run a command...
tor-prompt --run 'GETINFO version'
... or a file with a series of commands...
tor-prompt --run /home/atagar/tor_commands_to_run --- docs/change_log.rst | 4 ++++ stem/interpreter/__init__.py | 44 +++++++++++++++++++++----------------- stem/interpreter/arguments.py | 10 ++++++++- stem/interpreter/commands.py | 6 +++++- stem/interpreter/settings.cfg | 2 ++ stem/util/system.py | 2 +- test/integ/installation.py | 4 ++++ test/integ/interpreter.py | 35 ++++++++++++++++++++++++++++++ test/settings.cfg | 1 + test/unit/interpreter/arguments.py | 6 ++++++ 10 files changed, 91 insertions(+), 23 deletions(-)
diff --git a/docs/change_log.rst b/docs/change_log.rst index e9b6323..e988c61 100644 --- a/docs/change_log.rst +++ b/docs/change_log.rst @@ -65,6 +65,10 @@ The following are only available within Stem's `git repository * Added timeout argument to :func:`~stem.util.system.call` * Added :class:`~stem.util.test_tools.TimedTestRunner` and :func:`~stem.util.test_tools.test_runtimes`
+ * **Interpreter** + + * Added a '--run [command or path]' argument to invoke specific commands (:trac:`21541`) + .. _version_1.5:
Version 1.5 (November 20th, 2016) diff --git a/stem/interpreter/__init__.py b/stem/interpreter/__init__.py index 237a781..8cdbb3c 100644 --- a/stem/interpreter/__init__.py +++ b/stem/interpreter/__init__.py @@ -60,7 +60,7 @@ def main(): print(stem.interpreter.arguments.get_help()) sys.exit()
- if args.disable_color: + if args.disable_color or not sys.stdout.isatty(): global PROMPT stem.util.term.DISABLE_COLOR_SUPPORT = True PROMPT = '>>> ' @@ -76,7 +76,8 @@ def main(): print(format(msg('msg.tor_unavailable'), *ERROR_OUTPUT)) sys.exit(1) else: - print(format(msg('msg.starting_tor'), *HEADER_OUTPUT)) + if not args.run_cmd and not args.run_path: + print(format(msg('msg.starting_tor'), *HEADER_OUTPUT))
control_port = '9051' if args.control_port == 'default' else str(args.control_port)
@@ -124,25 +125,28 @@ def main():
interpreter = stem.interpreter.commands.ControlInterpreter(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: + if args.run_cmd: + interpreter.run_command(args.run_cmd, print_response = True) + elif args.run_path: try: - prompt = '... ' if interpreter.is_multiline_context else PROMPT + for line in open(args.run_path).readlines(): + interpreter.run_command(line.strip(), print_response = True) + except IOError as exc: + print(format(msg('msg.unable_to_read_file', path = args.run_path, error = exc), *ERROR_OUTPUT)) + sys.exit(1)
- if stem.prereq.is_python_3(): - user_input = input(prompt) - else: - user_input = raw_input(prompt) + else: + for line in msg('msg.startup_banner').splitlines(): + line_format = HEADER_BOLD_OUTPUT if line.startswith(' ') else HEADER_OUTPUT + print(format(line, *line_format))
- response = interpreter.run_command(user_input) + print('')
- if response is not None: - print(response) - except (KeyboardInterrupt, EOFError, stem.SocketClosed) as exc: - print('') # move cursor to the following line - break + while True: + try: + prompt = '... ' if interpreter.is_multiline_context else PROMPT + user_input = input(prompt) if stem.prereq.is_python_3() else raw_input(prompt) + interpreter.run_command(user_input, print_response = True) + 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 index 010aa3f..3160183 100644 --- a/stem/interpreter/arguments.py +++ b/stem/interpreter/arguments.py @@ -7,6 +7,7 @@ Commandline argument parsing for our interpreter prompt.
import collections import getopt +import os
import stem.interpreter import stem.util.connection @@ -18,12 +19,14 @@ DEFAULT_ARGS = { 'control_socket': '/var/run/tor/control', 'user_provided_socket': False, 'tor_path': 'tor', + 'run_cmd': None, + 'run_path': None, 'disable_color': False, 'print_help': False, }
OPT = 'i:s:h' -OPT_EXPANDED = ['interface=', 'socket=', 'tor=', 'no-color', 'help'] +OPT_EXPANDED = ['interface=', 'socket=', 'tor=', 'run=', 'no-color', 'help']
def parse(argv): @@ -71,6 +74,11 @@ def parse(argv): args['user_provided_socket'] = True elif opt in ('--tor'): args['tor_path'] = arg + elif opt in ('--run'): + if os.path.exists(arg): + args['run_path'] = arg + else: + args['run_cmd'] = arg elif opt == '--no-color': args['disable_color'] = True elif opt in ('-h', '--help'): diff --git a/stem/interpreter/commands.py b/stem/interpreter/commands.py index 366f256..4d845a0 100644 --- a/stem/interpreter/commands.py +++ b/stem/interpreter/commands.py @@ -294,13 +294,14 @@ class ControlInterpreter(code.InteractiveConsole): return format(response, *STANDARD_OUTPUT)
@uses_settings - def run_command(self, command, config): + def run_command(self, command, config, print_response = False): """ 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 + :param bool print_response: prints the response to stdout if true
:returns: **list** out output lines, each line being a list of (msg, format) tuples @@ -374,4 +375,7 @@ class ControlInterpreter(code.InteractiveConsole):
output += '\n' # give ourselves an extra line before the next prompt
+ if print_response and output is not None: + print(output) + return output diff --git a/stem/interpreter/settings.cfg b/stem/interpreter/settings.cfg index 1c6b27f..cc8da37 100644 --- a/stem/interpreter/settings.cfg +++ b/stem/interpreter/settings.cfg @@ -18,6 +18,7 @@ msg.help | -s, --socket SOCKET_PATH attach using unix domain socket if present, | SOCKET_PATH defaults to: {socket} | --tor PATH tor binary if tor isn't already running +| --run executes the given command or file of commands | --no-color disables colorized output | -h, --help presents this help | @@ -43,6 +44,7 @@ msg.startup_banner
msg.tor_unavailable Tor isn't running and the command currently isn't in your PATH. msg.unable_to_start_tor Unable to start tor: {error} +msg.unable_to_read_file Unable to read {path}: {error}
msg.starting_tor |Tor isn't running. Starting a temporary Tor instance for our interpreter to diff --git a/stem/util/system.py b/stem/util/system.py index a3ec756..4b705f3 100644 --- a/stem/util/system.py +++ b/stem/util/system.py @@ -1072,7 +1072,7 @@ def call(command, default = UNDEFINED, ignore_exit_status = False, timeout = Non if isinstance(command, str): command_list = command.split(' ') else: - command_list = command + command_list = map(str, command)
exit_status, runtime, stdout, stderr = None, None, None, None start_time = time.time() diff --git a/test/integ/installation.py b/test/integ/installation.py index ad21fd0..4d68988 100644 --- a/test/integ/installation.py +++ b/test/integ/installation.py @@ -1,3 +1,7 @@ +""" +Tests installation of our library. +""" + import glob import os import shutil diff --git a/test/integ/interpreter.py b/test/integ/interpreter.py new file mode 100644 index 0000000..4681c57 --- /dev/null +++ b/test/integ/interpreter.py @@ -0,0 +1,35 @@ +""" +Tests invocation of our interpreter. +""" + +import os +import tempfile +import unittest + +import stem.util.system + +import test.runner +import test.util + +PROMPT_CMD = os.path.join(test.util.STEM_BASE, 'tor-prompt') + + +class TestInterpreter(unittest.TestCase): + def test_running_command(self): + expected = ['250-config-file=%s' % test.runner.get_runner().get_torrc_path(), '250 OK'] + self.assertEqual(expected, stem.util.system.call([PROMPT_CMD, '--interface', test.runner.CONTROL_PORT, '--run', 'GETINFO config-file'])) + + def test_running_file(self): + expected = [ + '250-config-file=%s' % test.runner.get_runner().get_torrc_path(), + '250 OK', + '', + '250-version=%s' % test.util.tor_version(), + '250 OK', + ] + + with tempfile.NamedTemporaryFile(prefix = 'test_commands.') as tmp: + tmp.write('GETINFO config-file\nGETINFO version') + tmp.flush() + + self.assertEqual(expected, stem.util.system.call([PROMPT_CMD, '--interface', test.runner.CONTROL_PORT, '--run', tmp.name])) diff --git a/test/settings.cfg b/test/settings.cfg index 1d011f9..11137b4 100644 --- a/test/settings.cfg +++ b/test/settings.cfg @@ -226,6 +226,7 @@ test.integ_tests |test.integ.util.proc.TestProc |test.integ.util.system.TestSystem |test.integ.installation.TestInstallation +|test.integ.interpreter.TestInterpreter |test.integ.descriptor.remote.TestDescriptorDownloader |test.integ.descriptor.server_descriptor.TestServerDescriptor |test.integ.descriptor.extrainfo_descriptor.TestExtraInfoDescriptor diff --git a/test/unit/interpreter/arguments.py b/test/unit/interpreter/arguments.py index 6a765b1..df81e7e 100644 --- a/test/unit/interpreter/arguments.py +++ b/test/unit/interpreter/arguments.py @@ -51,6 +51,12 @@ class TestArgumentParsing(unittest.TestCase): for invalid_input in invalid_inputs: self.assertRaises(ValueError, parse, ['--interface', invalid_input])
+ def test_run_with_command(self): + self.assertEqual('GETINFO version', parse(['--run', 'GETINFO version']).run_cmd) + + def test_run_with_path(self): + self.assertEqual(__file__, parse(['--run', __file__]).run_path) + def test_get_help(self): help_text = get_help() self.assertTrue('Interactive interpreter for Tor.' in help_text)