From 91fbaba9bb78fcf6ba3b4a40cb4d671b952f50f4 Mon Sep 17 00:00:00 2001 From: Kevin Phillips Date: Fri, 27 Apr 2018 21:17:37 -0300 Subject: [PATCH] Fixes #18 Added support for shell commands --- src/friendlyshell/base_shell.py | 22 ++++++++++- src/friendlyshell/command_complete_mixin.py | 8 +++- src/friendlyshell/shell_help_mixin.py | 19 ++++++++- tests/test_base_shell.py | 43 +++++++++++++++++++++ tests/test_help_mixin.py | 1 + 5 files changed, 89 insertions(+), 4 deletions(-) diff --git a/src/friendlyshell/base_shell.py b/src/friendlyshell/base_shell.py index da40bf2..8630bac 100644 --- a/src/friendlyshell/base_shell.py +++ b/src/friendlyshell/base_shell.py @@ -2,6 +2,7 @@ import os import sys import inspect +import subprocess import pyparsing as pp from six.moves import input from friendlyshell.command_parsers import default_line_parser @@ -70,7 +71,6 @@ def _get_input(self): :returns: the input line retrieved from the source :rtype: :class:`str` """ - line = None try: if self.input_stream: line = self.input_stream.readline() @@ -142,6 +142,22 @@ def _execute_command(self, func, parser): # garbage to the user self.debug(err, exc_info=True) + def _run_shell_command(self, cmd): + """Executes a shell command within the Friendly Shell environment + + :param str cmd: Shell command to execute + """ + self.debug("Running shell command %s", cmd) + try: + output = subprocess.check_output( + cmd, + shell=True, + stderr=subprocess.STDOUT) + self.info(output) + except subprocess.CalledProcessError as err: + self.info("Failed to run command %s: %s", err.cmd, err.returncode) + self.info(err.output) + def run(self, *_args, **kwargs): """Main entry point function that launches our command line interpreter @@ -163,6 +179,10 @@ def run(self, *_args, **kwargs): if not line: continue + if line[0] == "!": + self._run_shell_command(line[1:]) + continue + parser = self._parse_line(line) if parser is None: continue diff --git a/src/friendlyshell/command_complete_mixin.py b/src/friendlyshell/command_complete_mixin.py index 6777647..22b526d 100644 --- a/src/friendlyshell/command_complete_mixin.py +++ b/src/friendlyshell/command_complete_mixin.py @@ -290,6 +290,7 @@ def _complete_callback(self, token, index): Returns None if there are no matches for the given token """ try: + line = readline.get_line_buffer() # ------------------------- DEBUG OUTPUT --------------------------- # NOTE: The begidx and endidx parameters specify the start and end+1 # location of the sub-string @@ -303,7 +304,7 @@ def _complete_callback(self, token, index): self.debug('\t\tSelected token "%s"', token) self.debug('\t\tMatch to return "%s"', index) # All text currently entered at the prompt, less the prompt itself - self.debug('\t\tline "%s"', readline.get_line_buffer()) + self.debug('\t\tline "%s"', line) # represents the offset from the start of the string to the first # character in the token to process self.debug('\t\tBeginning index "%s"', readline.get_begidx()) @@ -315,7 +316,10 @@ def _complete_callback(self, token, index): # this index would be: len(line) + 1 self.debug('\t\tEnding index "%s"', readline.get_endidx()) # ------------------------------------------------------------------ - + if readline.get_line_buffer()[0] == "!": + self.debug( + "Processing subcommand '%s'. Skipping command expansion.", + line) if index != 0: if index >= len(self._latest_matches): self.debug('Completed auto completion routine.') diff --git a/src/friendlyshell/shell_help_mixin.py b/src/friendlyshell/shell_help_mixin.py index 42e80b8..63ae7d2 100644 --- a/src/friendlyshell/shell_help_mixin.py +++ b/src/friendlyshell/shell_help_mixin.py @@ -51,8 +51,23 @@ def _list_commands(self): else: command_list['Extended Help'].append('N/A') + self.info("COMMANDS") self.info(tabulate.tabulate(command_list, headers="keys")) + def _list_operators(self): + """Displays a list of built-in operators supported by Friendly Shell""" + operator_list = { + 'Operator': ["!"], + 'Description': [ + "Redirects command to the native console" + ], + 'Examples': [ + "'!dir /ah', '!ls -alh'" + ] + } + self.info("OPERATORS") + self.info(tabulate.tabulate(operator_list, headers="keys")) + def do_help(self, arg=None): """Online help generation (this command) @@ -62,8 +77,10 @@ def do_help(self, arg=None): """ # no command given, show available commands if arg is None: - self.debug("Showing help for available commands...") + self.debug("Showing default help output...") self._list_commands() + self.info("\n") + self._list_operators() return # Sanity check: make sure we're asking for help for a command diff --git a/tests/test_base_shell.py b/tests/test_base_shell.py index 0fe0e68..bd2e2fb 100644 --- a/tests/test_base_shell.py +++ b/tests/test_base_shell.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals import logging +import sys +import os from friendlyshell.base_shell import BaseShell from friendlyshell.basic_logger_mixin import BasicLoggerMixin from mock import patch @@ -310,5 +312,46 @@ def do_something(self, my_param1, my_param2): assert msg in caplog.text +def test_shell_command(caplog): + caplog.set_level(logging.INFO) + class test_class(BasicLoggerMixin, BaseShell): + pass + + obj = test_class() + if sys.platform.startswith("win"): + test_cmd = "dir /a" + else: + test_cmd = "ls -a" + + in_stream = StringIO("""!{0} + exit""".format(test_cmd)) + + obj.run(input_stream=in_stream) + + for cur_item in os.listdir("."): + assert cur_item in caplog.text + + +def test_shell_command_not_found(caplog): + caplog.set_level(logging.INFO) + expected_text = "Hello World" + class test_class(BasicLoggerMixin, BaseShell): + def do_something(self): + self.info(expected_text) + + obj = test_class() + expected_command = "fubarasdf1234" + in_stream = StringIO("""!{0} + something + exit""".format(expected_command)) + + obj.run(input_stream=in_stream) + + # Make sure the second command in the sequence rance + assert expected_text in caplog.text + if sys.platform.startswith("win"): + assert "not recognized" in caplog.text + assert expected_command in caplog.text + if __name__ == "__main__": pytest.main([__file__, "-v", "-s"]) diff --git a/tests/test_help_mixin.py b/tests/test_help_mixin.py index a394f6b..4693309 100644 --- a/tests/test_help_mixin.py +++ b/tests/test_help_mixin.py @@ -21,6 +21,7 @@ def do_something(self): assert 'exit' in caplog.text assert 'help' in caplog.text assert 'something' in caplog.text + assert '!' in caplog.text assert obj.do_something.__doc__ in caplog.text