Repository: python-cmd2/cmd2
Pull request #647 (2019-03-14 22:22:49)

Help now writes to stderr when no help information is found
Since ``StdSim.stderr`` is used to detect errors in a pyscript, it makes sense for us to be writing error strings to stderr,

I also removed '***' from the beginning of `help()` and `default()` error messages.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8bde2d33..6d21b33c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,17 @@
 * Enhancements
     * Added ability to include command name placeholders in the message printed when trying to run a disabled command.
         * See docstring for ``disable_command()`` or ``disable_category()`` for more details.
+    * Added instance attributes to customize error messages without having to override methods. Theses messages can
+    also be colored.
+        * `help_error` - the error that prints when no help information can be found
+        * `default_error` - the error that prints when a non-existent command is run
+* Potentially breaking changes
+    * The following commands now write to stderr instead of stdout when printing an error. This will make catching
+    errors easier in pyscript.
+        * ``do_help()`` - when no help information can be found
+        * ``default()`` - in all cases since this is called when an invalid command name is run
+        * ``_report_disabled_command_usage()`` - in all cases since this is called when a disabled command is run
+    * Removed *** from beginning of error messages printed by `do_help()` and `default()`.
 
 ## 0.9.11 (March 13, 2019)
 * Bug Fixes
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 6b085a98..e41d947d 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -428,6 +428,12 @@ class Cmd(cmd.Cmd):
         # Used to keep track of whether a continuation prompt is being displayed
         self.at_continuation_prompt = False
 
+        # The error that prints when no help information can be found
+        self.help_error = "No help on {}"
+
+        # The error that prints when a non-existent command is run
+        self.default_error = "{} is not a recognized command, alias, or macro"
+
         # If this string is non-empty, then this warning message will print if a broken pipe error occurs while printing
         self.broken_pipe_warning = ''
 
@@ -2057,8 +2063,8 @@ class Cmd(cmd.Cmd):
 
             return self.do_shell(statement.command_and_args)
         else:
-            self.perror('*** {} is not a recognized command, alias, or macro'.format(statement.command),
-                        err_color=Fore.RESET, traceback_war=False)
+            err_msg = self.default_error.format(statement.command)
+            self.decolorized_write(sys.stderr, "{}\n".format(err_msg))
 
     def pseudo_raw_input(self, prompt: str) -> str:
         """Began life as a copy of cmd's cmdloop; like raw_input but
@@ -2586,12 +2592,21 @@ class Cmd(cmd.Cmd):
         else:
             # Getting help for a specific command
             func = self.cmd_func(args.command)
+            help_func = getattr(self, HELP_FUNC_PREFIX + args.command, None)
+
+            # If the command function uses argparse, then use argparse's help
             if func and hasattr(func, 'argparser'):
                 completer = AutoCompleter(getattr(func, 'argparser'), self)
                 tokens = [args.command] + args.subcommand
                 self.poutput(completer.format_help(tokens))
+
+            # If there is no help information then print an error
+            elif help_func is None and (func is None or not func.__doc__):
+                err_msg = self.help_error.format(args.command)
+                self.decolorized_write(sys.stderr, "{}\n".format(err_msg))
+
+            # Otherwise delegate to cmd base class do_help()
             else:
-                # No special behavior needed, delegate to cmd base class do_help()
                 super().do_help(args.command)
 
     def _help_menu(self, verbose: bool = False) -> None:
@@ -3719,7 +3734,7 @@ class Cmd(cmd.Cmd):
         :param message_to_print: the message reporting that the command is disabled
         :param kwargs: not used
         """
-        self.perror(message_to_print, err_color=Fore.RESET, traceback_war=False)
+        self.decolorized_write(sys.stderr, "{}\n".format(message_to_print))
 
     def cmdloop(self, intro: Optional[str] = None) -> None:
         """This is an outer wrapper around _cmdloop() which deals with extra features provided by cmd2.


Test patch:
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index 44ca89e0..6733be19 100644
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -952,10 +952,10 @@ undoc
 """)
     assert out == expected
 
-def test_help_undocumented(help_app):
-    out = run_cmd(help_app, 'help undoc')
-    expected = normalize('*** No help on undoc')
-    assert out == expected
+def test_help_undocumented(help_app, capsys):
+    run_cmd(help_app, 'help undoc')
+    out, err = capsys.readouterr()
+    assert err.startswith("No help on undoc")
 
 def test_help_overridden_method(help_app):
     out = run_cmd(help_app, 'help edit')
@@ -1787,8 +1787,9 @@ def test_macro_create_with_escaped_args(base_app, capsys):
     assert out == normalize("Macro 'fake' created")
 
     # Run the macro
-    out = run_cmd(base_app, 'fake')
-    assert 'No help on {1}' in out[0]
+    run_cmd(base_app, 'fake')
+    out, err = capsys.readouterr()
+    assert err.startswith('No help on {1}')
 
 def test_macro_usage_with_missing_args(base_app, capsys):
     # Create the macro