Repository: python-cmd2/cmd2
Pull request #604 (2018-11-30 23:57:52)

Made it so default_to_shell results in do_shell being called so that output can be captured
This partially addresses #545 


Patch:
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 2a617a75..012b6f6b 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -632,7 +632,7 @@ class Cmd(cmd.Cmd):
         err_msg = err_color + err_msg + Fore.RESET
         self.decolorized_write(sys.stderr, err_msg)
 
-        if traceback_war:
+        if traceback_war and not self.debug:
             war = "To enable full traceback, run the following command:  'set debug true'\n"
             war = war_color + war + Fore.RESET
             self.decolorized_write(sys.stderr, war)
@@ -1893,6 +1893,8 @@ class Cmd(cmd.Cmd):
             try:
                 self.pipe_proc = subprocess.Popen(statement.pipe_to, stdin=subproc_stdin)
             except Exception as ex:
+                self.perror('Not piping because - {}'.format(ex), traceback_war=False)
+
                 # Restore stdout to what it was and close the pipe
                 self.stdout.close()
                 subproc_stdin.close()
@@ -1901,8 +1903,6 @@ class Cmd(cmd.Cmd):
                 self.kept_state = None
                 self.redirecting = False
 
-                # Re-raise the exception
-                raise ex
         elif statement.output:
             import tempfile
             if (not statement.output_to) and (not self.can_clip):
@@ -1920,7 +1920,7 @@ class Cmd(cmd.Cmd):
                 try:
                     sys.stdout = self.stdout = open(statement.output_to, mode)
                 except OSError as ex:
-                    self.perror('Not Redirecting because - {}'.format(ex), traceback_war=False)
+                    self.perror('Not redirecting because - {}'.format(ex), traceback_war=False)
                     self.redirecting = False
             else:
                 # going to a paste buffer
@@ -2007,8 +2007,10 @@ class Cmd(cmd.Cmd):
                 stop = func(statement)
 
             else:
-                self.default(statement)
-                stop = False
+                stop = self.default(statement)
+
+        if stop is None:
+            stop = False
 
         return stop
 
@@ -2056,19 +2058,18 @@ class Cmd(cmd.Cmd):
         # Run the resolved command
         return self.onecmd_plus_hooks(resolved)
 
-    def default(self, statement: Statement) -> None:
+    def default(self, statement: Statement) -> Optional[bool]:
         """Executed when the command given isn't a recognized command implemented by a do_* method.
 
         :param statement: Statement object with parsed input
         """
         if self.default_to_shell:
-            result = os.system(statement.command_and_args)
-            # If os.system() succeeded, then don't print warning about unknown command
-            if not result:
-                return
+            if 'shell' not in self.exclude_from_history:
+                self.history.append(statement.raw)
 
-        # Print out a message stating this is an unknown command
-        self.poutput('*** Unknown syntax: {}\n'.format(statement.command_and_args))
+            return self.do_shell(statement.command_and_args)
+        else:
+            self.poutput('*** {} is not a recognized command, alias, or macro\n'.format(statement.command))
 
     def pseudo_raw_input(self, prompt: str) -> str:
         """Began life as a copy of cmd's cmdloop; like raw_input but
@@ -2803,7 +2804,7 @@ class Cmd(cmd.Cmd):
         :param args: argparse parsed arguments from the set command
         :param parameter: optional search parameter
         """
-        param = parameter.strip().lower()
+        param = utils.norm_fold(parameter.strip())
         result = {}
         maxlen = 0
 
@@ -2844,7 +2845,7 @@ class Cmd(cmd.Cmd):
         # Check if param was passed in
         if not args.param:
             return self.show(args)
-        param = args.param.strip().lower()
+        param = utils.norm_fold(args.param.strip())
 
         # Check if value was passed in
         if not args.value:
@@ -3156,15 +3157,19 @@ class Cmd(cmd.Cmd):
     history_parser_group.add_argument('-e', '--edit', action='store_true',
                                       help='edit and then run selected history items')
     history_parser_group.add_argument('-s', '--script', action='store_true', help='output commands in script format')
-    history_parser_group.add_argument('-o', '--output-file', metavar='FILE', help='output commands to a script file')
-    history_parser_group.add_argument('-t', '--transcript', help='output commands and results to a transcript file')
+    setattr(history_parser_group.add_argument('-o', '--output-file', metavar='FILE',
+                                              help='output commands to a script file'),
+            ACTION_ARG_CHOICES, ('path_complete',))
+    setattr(history_parser_group.add_argument('-t', '--transcript',
+                                              help='output commands and results to a transcript file'),
+            ACTION_ARG_CHOICES, ('path_complete',))
     history_parser_group.add_argument('-c', '--clear', action="store_true", help='clear all history')
-    _history_arg_help = """empty               all history items
-a                   one history item by number
-a..b, a:b, a:, ..b  items by indices (inclusive)
-[string]            items containing string
-/regex/             items matching regular expression"""
-    history_parser.add_argument('arg', nargs='?', help=_history_arg_help)
+    history_arg_help = ("empty               all history items\n"
+                        "a                   one history item by number\n"
+                        "a..b, a:b, a:, ..b  items by indices (inclusive)\n"
+                        "string              items containing string\n"
+                        "/regex/             items matching regular expression")
+    history_parser.add_argument('arg', nargs='?', help=history_arg_help)
 
     @with_argparser(history_parser)
     def do_history(self, args: argparse.Namespace) -> None:
@@ -3851,7 +3856,6 @@ class History(list):
                     end = int(end)
                 return self[start:end]
 
-            # noinspection PyUnresolvedReferences
             getme = getme.strip()
 
             if getme.startswith(r'/') and getme.endswith(r'/'):
@@ -3871,7 +3875,7 @@ class History(list):
                     :param hi: HistoryItem
                     :return: bool - True if search matches
                     """
-                    return getme.lower() in hi.lowercase
+                    return utils.norm_fold(getme) in utils.norm_fold(hi)
             return [itm for itm in self if isin(itm)]
 
 


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index da7e8b08..ac6a1896 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -59,7 +59,7 @@ positional arguments:
   arg                   empty               all history items
                         a                   one history item by number
                         a..b, a:b, a:, ..b  items by indices (inclusive)
-                        [string]            items containing string
+                        string              items containing string
                         /regex/             items matching regular expression
 
 optional arguments:
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index 9bb85ffe..59c6bb60 100644
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -298,7 +298,7 @@ def test_pyscript_requires_an_argument(base_app, capsys):
 
 def test_base_error(base_app):
     out = run_cmd(base_app, 'meow')
-    assert out == ["*** Unknown syntax: meow"]
+    assert "is not a recognized command" in out[0]
 
 
 @pytest.fixture
@@ -803,8 +803,7 @@ def test_pipe_to_shell_error(base_app, capsys):
     run_cmd(base_app, 'help | foobarbaz.this_does_not_exist')
     out, err = capsys.readouterr()
     assert not out
-    expected_error = 'FileNotFoundError'
-    assert err.startswith("EXCEPTION of type '{}' occurred with message:".format(expected_error))
+    assert err.startswith("ERROR: Not piping because")
 
 
 @pytest.mark.skipif(not clipboard.can_clip,
@@ -1042,12 +1041,12 @@ def hook_failure():
 
 def test_precmd_hook_success(base_app):
     out = base_app.onecmd_plus_hooks('help')
-    assert out is None
+    assert out is False
 
 
 def test_precmd_hook_failure(hook_failure):
     out = hook_failure.onecmd_plus_hooks('help')
-    assert out == True
+    assert out is True
 
 
 class SayApp(cmd2.Cmd):
@@ -1098,40 +1097,18 @@ class ShellApp(cmd2.Cmd):
         super().__init__(*args, **kwargs)
         self.default_to_shell = True
 
-@pytest.fixture
-def shell_app():
-    app = ShellApp()
-    app.stdout = utils.StdSim(app.stdout)
-    return app
-
-def test_default_to_shell_unknown(shell_app):
-    unknown_command = 'zyxcw23'
-    out = run_cmd(shell_app, unknown_command)
-    assert out == ["*** Unknown syntax: {}".format(unknown_command)]
-
-def test_default_to_shell_good(capsys):
-    app = cmd2.Cmd()
-    app.default_to_shell = True
+def test_default_to_shell(base_app, monkeypatch):
     if sys.platform.startswith('win'):
         line = 'dir'
     else:
         line = 'ls'
-    statement = app.statement_parser.parse(line)
-    retval = app.default(statement)
-    assert not retval
-    out, err = capsys.readouterr()
-    assert out == ''
-
-def test_default_to_shell_failure(capsys):
-    app = cmd2.Cmd()
-    app.default_to_shell = True
-    line = 'ls does_not_exist.xyz'
-    statement = app.statement_parser.parse(line)
-    retval = app.default(statement)
-    assert not retval
-    out, err = capsys.readouterr()
-    assert out == "*** Unknown syntax: {}\n".format(line)
 
+    base_app.default_to_shell = True
+    m = mock.Mock()
+    monkeypatch.setattr("{}.Popen".format('subprocess'), m)
+    out = run_cmd(base_app, line)
+    assert out == []
+    assert m.called
 
 def test_ansi_prompt_not_esacped(base_app):
     from cmd2.rl_utils import rl_make_safe_prompt