summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKevin Van Brunt <kmvanbrunt@gmail.com>2018-09-26 14:37:13 -0400
committerKevin Van Brunt <kmvanbrunt@gmail.com>2018-09-26 14:37:13 -0400
commit8aeb29cf1fd027093b87b8f9f9c640cf50595db7 (patch)
tree23cb1ef06b4f9e52fe8b6da0b7e9f51a05e609e1
parent149f6eba2620b1623f6071227318450c779b2b50 (diff)
parentc8983d9d6df4d057672166a7e8df544199788b9a (diff)
downloadcmd2-git-8aeb29cf1fd027093b87b8f9f9c640cf50595db7.tar.gz
Merge branch 'macro' into argparse_conversion
-rw-r--r--CHANGELOG.md9
-rw-r--r--CONTRIBUTING.md2
-rwxr-xr-xcmd2/argparse_completer.py3
-rw-r--r--cmd2/cmd2.py295
-rw-r--r--cmd2/constants.py5
-rw-r--r--cmd2/parsing.py13
-rw-r--r--cmd2/utils.py12
-rw-r--r--docs/settingchanges.rst4
-rw-r--r--docs/unfreefeatures.rst40
-rwxr-xr-xexamples/colors.py142
-rwxr-xr-xexamples/pirate.py20
-rwxr-xr-xexamples/plumbum_colors.py144
-rwxr-xr-xexamples/python_scripting.py4
-rw-r--r--examples/transcripts/exampleSession.txt2
-rw-r--r--examples/transcripts/transcript_regex.txt2
-rw-r--r--tests/conftest.py18
-rw-r--r--tests/scripts/postcmds.txt2
-rw-r--r--tests/scripts/precmds.txt2
-rw-r--r--tests/test_cmd2.py209
-rw-r--r--tests/transcripts/regex_set.txt4
20 files changed, 743 insertions, 189 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f5dff203..3015b793 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,15 @@
* These allow you to provide feedback to the user in an asychronous fashion, meaning alerts can
display when the user is still entering text at the prompt. See [async_printing.py](https://github.com/python-cmd2/cmd2/blob/master/examples/async_printing.py)
for an example.
+ * Cross-platform colored output support
+ * ``colorama`` gets initialized properly in ``Cmd.__init()``
+ * The ``Cmd.colors`` setting is no longer platform dependent and now has three values:
+ * Terminal (default) - output methods do not strip any ANSI escape sequences when output is a terminal, but
+ if the output is a pipe or a file the escape sequences are stripped
+ * Always - output methods **never** strip ANSI escape sequences, regardless of the output destination
+ * Never - output methods strip all ANSI escape sequences
+* Deprecations
+ * Deprecated the builtin ``cmd2`` suport for colors including ``Cmd.colorize()`` and ``Cmd._colorcodes``
* Deletions
* The ``preparse``, ``postparsing_precmd``, and ``postparsing_postcmd`` methods *deprecated* in the previous release
have been deleted
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e4de4fd4..5ba66b14 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -12,7 +12,7 @@ Remember to feel free to ask for help by leaving a comment within the Issue.
Working on your first pull request? You can learn how from this *free* series [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github).
-###### If you've found a bug that is not on the board, [follow these steps](Readme.md#found-a-bug).
+###### If you've found a bug that is not on the board, [follow these steps](README.md#found-a-bug).
--------------------------------------------------------------------------------
diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py
index 21e116ed..ca50bba9 100755
--- a/cmd2/argparse_completer.py
+++ b/cmd2/argparse_completer.py
@@ -891,6 +891,9 @@ class ACHelpFormatter(argparse.RawTextHelpFormatter):
result = super()._format_args(action, default_metavar)
return result
+ def format_help(self):
+ return super().format_help() + '\n'
+
# noinspection PyCompatibility
class ACArgumentParser(argparse.ArgumentParser):
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 292969ca..f40b1508 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -32,16 +32,16 @@ Git repository on GitHub at https://github.com/python-cmd2/cmd2
import argparse
import cmd
import collections
+import colorama
from colorama import Fore
import glob
import inspect
import os
-import platform
import re
import shlex
import sys
import threading
-from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple, Type, Union
+from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple, Type, Union, IO
from . import constants
from . import utils
@@ -139,19 +139,21 @@ def categorize(func: Union[Callable, Iterable], category: str) -> None:
setattr(func, HELP_CATEGORY, category)
-def parse_quoted_string(cmdline: str) -> List[str]:
- """Parse a quoted string into a list of arguments."""
- if isinstance(cmdline, list):
+def parse_quoted_string(string: str, preserve_quotes: bool) -> List[str]:
+ """
+ Parse a quoted string into a list of arguments
+ :param string: the string being parsed
+ :param preserve_quotes: if True, then quotes will not be stripped
+ """
+ if isinstance(string, list):
# arguments are already a list, return the list we were passed
- lexed_arglist = cmdline
+ lexed_arglist = string
else:
# Use shlex to split the command line into a list of arguments based on shell rules
- lexed_arglist = shlex.split(cmdline, posix=False)
- # strip off outer quotes for convenience
- temp_arglist = []
- for arg in lexed_arglist:
- temp_arglist.append(utils.strip_quotes(arg))
- lexed_arglist = temp_arglist
+ lexed_arglist = shlex.split(string, posix=False)
+
+ if not preserve_quotes:
+ lexed_arglist = [utils.strip_quotes(arg) for arg in lexed_arglist]
return lexed_arglist
@@ -163,7 +165,7 @@ def with_category(category: str) -> Callable:
return cat_decorator
-def with_argument_list(func: Callable) -> Callable:
+def with_argument_list(func: Callable, preserve_quotes: bool=False) -> Callable:
"""A decorator to alter the arguments passed to a do_* cmd2
method. Default passes a string of whatever the user typed.
With this decorator, the decorated method will receive a list
@@ -172,18 +174,19 @@ def with_argument_list(func: Callable) -> Callable:
@functools.wraps(func)
def cmd_wrapper(self, cmdline):
- lexed_arglist = parse_quoted_string(cmdline)
+ lexed_arglist = parse_quoted_string(cmdline, preserve_quotes)
return func(self, lexed_arglist)
cmd_wrapper.__doc__ = func.__doc__
return cmd_wrapper
-def with_argparser_and_unknown_args(argparser: argparse.ArgumentParser) -> Callable:
+def with_argparser_and_unknown_args(argparser: argparse.ArgumentParser, preserve_quotes: bool=False) -> Callable:
"""A decorator to alter a cmd2 method to populate its ``args`` argument by parsing arguments with the given
instance of argparse.ArgumentParser, but also returning unknown args as a list.
:param argparser: given instance of ArgumentParser
+ :param preserve_quotes: if True, then the arguments passed to arparse be maintain their quotes
:return: function that gets passed parsed args and a list of unknown args
"""
import functools
@@ -192,7 +195,7 @@ def with_argparser_and_unknown_args(argparser: argparse.ArgumentParser) -> Calla
def arg_decorator(func: Callable):
@functools.wraps(func)
def cmd_wrapper(instance, cmdline):
- lexed_arglist = parse_quoted_string(cmdline)
+ lexed_arglist = parse_quoted_string(cmdline, preserve_quotes)
try:
args, unknown = argparser.parse_known_args(lexed_arglist)
except SystemExit:
@@ -221,11 +224,12 @@ def with_argparser_and_unknown_args(argparser: argparse.ArgumentParser) -> Calla
return arg_decorator
-def with_argparser(argparser: argparse.ArgumentParser) -> Callable:
+def with_argparser(argparser: argparse.ArgumentParser, preserve_quotes: bool=False) -> Callable:
"""A decorator to alter a cmd2 method to populate its ``args`` argument by parsing arguments
with the given instance of argparse.ArgumentParser.
:param argparser: given instance of ArgumentParser
+ :param preserve_quotes: if True, then the arguments passed to arparse be maintain their quotes
:return: function that gets passed parsed args
"""
import functools
@@ -234,7 +238,7 @@ def with_argparser(argparser: argparse.ArgumentParser) -> Callable:
def arg_decorator(func: Callable):
@functools.wraps(func)
def cmd_wrapper(instance, cmdline):
- lexed_arglist = parse_quoted_string(cmdline)
+ lexed_arglist = parse_quoted_string(cmdline, preserve_quotes)
try:
args = argparser.parse_args(lexed_arglist)
except SystemExit:
@@ -320,7 +324,7 @@ class Cmd(cmd.Cmd):
reserved_words = []
# Attributes which ARE dynamically settable at runtime
- colors = (platform.system() != 'Windows')
+ colors = constants.COLORS_TERMINAL
continuation_prompt = '> '
debug = False
echo = False
@@ -340,7 +344,7 @@ class Cmd(cmd.Cmd):
# To make an attribute settable with the "do_set" command, add it to this ...
# This starts out as a dictionary but gets converted to an OrderedDict sorted alphabetically by key
- settable = {'colors': 'Colorized output (*nix only)',
+ settable = {'colors': 'Allow colorized output (valid values: Terminal, Always, Never)',
'continuation_prompt': 'On 2nd+ line of input',
'debug': 'Show full error stack on error',
'echo': 'Echo command issued into output',
@@ -372,6 +376,9 @@ class Cmd(cmd.Cmd):
except AttributeError:
pass
+ # Override whether ansi codes should be stripped from the output since cmd2 has its own logic for doing this
+ colorama.init(strip=False)
+
# initialize plugin system
# needs to be done before we call __init__(0)
self._initialize_plugin_system()
@@ -420,13 +427,13 @@ class Cmd(cmd.Cmd):
self._STOP_AND_EXIT = True # cmd convention
self._colorcodes = {'bold': {True: '\x1b[1m', False: '\x1b[22m'},
- 'cyan': {True: '\x1b[36m', False: '\x1b[39m'},
- 'blue': {True: '\x1b[34m', False: '\x1b[39m'},
- 'red': {True: '\x1b[31m', False: '\x1b[39m'},
- 'magenta': {True: '\x1b[35m', False: '\x1b[39m'},
- 'green': {True: '\x1b[32m', False: '\x1b[39m'},
- 'underline': {True: '\x1b[4m', False: '\x1b[24m'},
- 'yellow': {True: '\x1b[33m', False: '\x1b[39m'}}
+ 'cyan': {True: Fore.CYAN, False: Fore.RESET},
+ 'blue': {True: Fore.BLUE, False: Fore.RESET},
+ 'red': {True: Fore.RED, False: Fore.RESET},
+ 'magenta': {True: Fore.MAGENTA, False: Fore.RESET},
+ 'green': {True: Fore.GREEN, False: Fore.RESET},
+ 'underline': {True: '\x1b[4m', False: Fore.RESET},
+ 'yellow': {True: Fore.YELLOW, False: Fore.RESET}}
# Used load command to store the current script dir as a LIFO queue to support _relative_load command
self._script_dir = []
@@ -556,34 +563,53 @@ class Cmd(cmd.Cmd):
# Make sure settable parameters are sorted alphabetically by key
self.settable = collections.OrderedDict(sorted(self.settable.items(), key=lambda t: t[0]))
- def poutput(self, msg: str, end: str='\n') -> None:
- """Convenient shortcut for self.stdout.write(); by default adds newline to end if not already present.
+ def decolorized_write(self, fileobj: IO, msg: str) -> None:
+ """Write a string to a fileobject, stripping ANSI escape sequences if necessary
- Also handles BrokenPipeError exceptions for when a commands's output has been piped to another process and
- that process terminates before the cmd2 command is finished executing.
+ Honor the current colors setting, which requires us to check whether the
+ fileobject is a tty.
+ """
+ if self.colors.lower() == constants.COLORS_NEVER.lower() or \
+ (self.colors.lower() == constants.COLORS_TERMINAL.lower() and not fileobj.isatty()):
+ msg = utils.strip_ansi(msg)
+ fileobj.write(msg)
+
+ def poutput(self, msg: Any, end: str='\n', color: str='') -> None:
+ """Smarter self.stdout.write(); color aware and adds newline of not present.
+
+ Also handles BrokenPipeError exceptions for when a commands's output has
+ been piped to another process and that process terminates before the
+ cmd2 command is finished executing.
:param msg: message to print to current stdout (anything convertible to a str with '{}'.format() is OK)
- :param end: string appended after the end of the message if not already present, default a newline
+ :param end: (optional) string appended after the end of the message if not already present, default a newline
+ :param color: (optional) color escape to output this message with
"""
if msg is not None and msg != '':
try:
msg_str = '{}'.format(msg)
- self.stdout.write(msg_str)
if not msg_str.endswith(end):
- self.stdout.write(end)
+ msg_str += end
+ if color:
+ msg_str = color + msg_str + Fore.RESET
+ self.decolorized_write(self.stdout, msg_str)
except BrokenPipeError:
- # This occurs if a command's output is being piped to another process and that process closes before the
- # command is finished. If you would like your application to print a warning message, then set the
- # broken_pipe_warning attribute to the message you want printed.
+ # This occurs if a command's output is being piped to another
+ # process and that process closes before the command is
+ # finished. If you would like your application to print a
+ # warning message, then set the broken_pipe_warning attribute
+ # to the message you want printed.
if self.broken_pipe_warning:
sys.stderr.write(self.broken_pipe_warning)
- def perror(self, err: Union[str, Exception], traceback_war: bool=True) -> None:
+ def perror(self, err: Union[str, Exception], traceback_war: bool=True, err_color: str=Fore.LIGHTRED_EX,
+ war_color: str=Fore.LIGHTYELLOW_EX) -> None:
""" Print error message to sys.stderr and if debug is true, print an exception Traceback if one exists.
:param err: an Exception or error message to print out
:param traceback_war: (optional) if True, print a message to let user know they can enable debug
- :return:
+ :param err_color: (optional) color escape to output error with
+ :param war_color: (optional) color escape to output warning with
"""
if self.debug:
import traceback
@@ -591,14 +617,15 @@ class Cmd(cmd.Cmd):
if isinstance(err, Exception):
err_msg = "EXCEPTION of type '{}' occurred with message: '{}'\n".format(type(err).__name__, err)
- sys.stderr.write(self.colorize(err_msg, 'red'))
else:
- err_msg = self.colorize("ERROR: {}\n".format(err), 'red')
- sys.stderr.write(err_msg)
+ err_msg = "ERROR: {}\n".format(err)
+ err_msg = err_color + err_msg + Fore.RESET
+ self.decolorized_write(sys.stderr, err_msg)
if traceback_war:
war = "To enable full traceback, run the following command: 'set debug true'\n"
- sys.stderr.write(self.colorize(war, 'yellow'))
+ war = war_color + war + Fore.RESET
+ self.decolorized_write(sys.stderr, war)
def pfeedback(self, msg: str) -> None:
"""For printing nonessential feedback. Can be silenced with `quiet`.
@@ -607,7 +634,7 @@ class Cmd(cmd.Cmd):
if self.feedback_to_output:
self.poutput(msg)
else:
- sys.stderr.write("{}\n".format(msg))
+ self.decolorized_write(sys.stderr, "{}\n".format(msg))
def ppaged(self, msg: str, end: str='\n', chop: bool=False) -> None:
"""Print output using a pager if it would go off screen and stdout isn't currently being redirected.
@@ -643,6 +670,9 @@ class Cmd(cmd.Cmd):
# Don't attempt to use a pager that can block if redirecting or running a script (either text or Python)
# Also only attempt to use a pager if actually running in a real fully functional terminal
if functional_terminal and not self.redirecting and not self._in_py and not self._script_dir:
+ if self.colors.lower() == constants.COLORS_NEVER.lower():
+ msg_str = utils.strip_ansi(msg_str)
+
pager = self.pager
if chop:
pager = self.pager_chop
@@ -667,7 +697,7 @@ class Cmd(cmd.Cmd):
except BrokenPipeError:
# This occurs if a command's output is being piped to another process and that process closes before the
# command is finished. If you would like your application to print a warning message, then set the
- # broken_pipe_warning attribute to the message you want printed.
+ # broken_pipe_warning attribute to the message you want printed.`
if self.broken_pipe_warning:
sys.stderr.write(self.broken_pipe_warning)
@@ -678,7 +708,7 @@ class Cmd(cmd.Cmd):
is running on Windows, will return ``val`` unchanged.
``color`` should be one of the supported strings (or styles):
red/blue/green/cyan/magenta, bold, underline"""
- if self.colors and (self.stdout == self.initial_stdout):
+ if self.colors.lower() != constants.COLORS_NEVER.lower() and (self.stdout == self.initial_stdout):
return self._colorcodes[color][True] + val + self._colorcodes[color][False]
return val
@@ -1452,6 +1482,10 @@ class Cmd(cmd.Cmd):
except AttributeError:
compfunc = self.completedefault
+ # Check if a macro was entered
+ elif command in self.macros:
+ compfunc = self.path_complete
+
# A valid command was not entered
else:
# Check if this command should be run as a shell command
@@ -2201,9 +2235,9 @@ class Cmd(cmd.Cmd):
def alias_create(self, args: argparse.Namespace):
""" Creates or overwrites an alias """
# Validate the alias name
- valid, errmsg = self.statement_parser.is_valid_command(args.name)
+ valid, errmsg = self.statement_parser.is_valid_command(args.name, allow_shortcut=False)
if not valid:
- errmsg = "Alias names {}".format(errmsg)
+ errmsg = "Invalid alias name: {}".format(errmsg)
self.perror(errmsg, traceback_war=False)
return
@@ -2212,19 +2246,23 @@ class Cmd(cmd.Cmd):
self.perror(errmsg, traceback_war=False)
return
- if not args.command.strip():
- errmsg = "Aliases cannot resolve to an empty command"
+ valid, errmsg = self.statement_parser.is_valid_command(args.command, allow_shortcut=True)
+ if not valid:
+ errmsg = "Invalid alias target: {}".format(errmsg)
self.perror(errmsg, traceback_war=False)
return
+ utils.unquote_redirection_tokens(args.command_args)
+
# Build the alias value string
- value = utils.quote_string_if_needed(args.command)
+ value = args.command
for cur_arg in args.command_args:
- value += ' ' + utils.quote_string_if_needed(cur_arg)
+ value += ' ' + cur_arg
# Set the alias
+ result = "overwritten" if args.name in self.aliases else "created"
self.aliases[args.name] = value
- self.poutput("Alias {!r} created".format(args.name))
+ self.poutput("Alias {!r} {}".format(args.name, result))
def alias_delete(self, args: argparse.Namespace):
""" Deletes aliases """
@@ -2259,7 +2297,12 @@ class Cmd(cmd.Cmd):
self.poutput("alias create {} {}".format(cur_alias, self.aliases[cur_alias]))
# Top-level parser for alias
- alias_parser = ACArgumentParser(description="Manage aliases", prog='alias')
+ alias_description = ("Manage aliases\n"
+ "\n"
+ "An alias is a command that enables replacement of a word by another string.")
+ alias_epilog = ("See also:\n"
+ " macro")
+ alias_parser = ACArgumentParser(description=alias_description, epilog=alias_epilog, prog='alias')
# Add subcommands to alias
alias_subparsers = alias_parser.add_subparsers()
@@ -2268,14 +2311,17 @@ class Cmd(cmd.Cmd):
alias_create_help = "create or overwrite an alias"
alias_create_description = "Create or overwrite an alias"
- alias_create_epilog = "Notes:\n"
- alias_create_epilog += " If you want to use redirection or pipes in the alias, then quote them to prevent\n"
- alias_create_epilog += " the alias command itself from being redirected\n"
- alias_create_epilog += "\n"
- alias_create_epilog += "Examples:\n"
- alias_create_epilog += " alias ls !ls -lF\n"
- alias_create_epilog += " alias create show_log !cat \"log file.txt\"\n"
- alias_create_epilog += " alias create save_results print_results \">\" out.txt\n"
+ alias_create_epilog = ("Notes:\n"
+ " If you want to use redirection or pipes in the alias, then quote them to prevent\n"
+ " the 'alias create' command itself from being redirected\n"
+ "\n"
+ " Since aliases are resolved during parsing, tab completion will function as it would\n"
+ " for the actual command the alias resolves to."
+ "\n"
+ "Examples:\n"
+ " alias ls !ls -lF\n"
+ " alias create show_log !cat \"log file.txt\"\n"
+ " alias create save_results print_results \">\" out.txt\n")
alias_create_parser = alias_subparsers.add_parser('create', help=alias_create_help,
description=alias_create_description,
@@ -2301,10 +2347,10 @@ class Cmd(cmd.Cmd):
# alias -> list
alias_list_help = "list aliases"
- alias_list_description = "List specified aliases in a reusable form that can be saved to\n"
- alias_list_description += "a startup_script to preserve aliases across sessions\n"
- alias_list_description += "\n"
- alias_list_description += "Without arguments, all aliases will be listed"
+ alias_list_description = ("List specified aliases in a reusable form that can be saved to\n"
+ "a startup_script to preserve aliases across sessions\n"
+ "\n"
+ "Without arguments, all aliases will be listed")
alias_list_parser = alias_subparsers.add_parser('list', help=alias_list_help,
description=alias_list_description)
@@ -2312,7 +2358,7 @@ class Cmd(cmd.Cmd):
ACTION_ARG_CHOICES, aliases)
alias_list_parser.set_defaults(func=alias_list)
- @with_argparser(alias_parser)
+ @with_argparser(alias_parser, preserve_quotes=True)
def do_alias(self, args: argparse.Namespace):
"""Manage aliases"""
func = getattr(args, 'func', None)
@@ -2328,9 +2374,9 @@ class Cmd(cmd.Cmd):
def macro_create(self, args: argparse.Namespace):
""" Creates or overwrites a macro """
# Validate the macro name
- valid, errmsg = self.statement_parser.is_valid_command(args.name)
+ valid, errmsg = self.statement_parser.is_valid_command(args.name, allow_shortcut=False)
if not valid:
- errmsg = "Macro names {}".format(errmsg)
+ errmsg = "Invalid macro name: {}".format(errmsg)
self.perror(errmsg, traceback_war=False)
return
@@ -2344,15 +2390,18 @@ class Cmd(cmd.Cmd):
self.perror(errmsg, traceback_war=False)
return
- if not args.command.strip():
- errmsg = "Macros cannot resolve to an empty command"
+ valid, errmsg = self.statement_parser.is_valid_command(args.command, allow_shortcut=True)
+ if not valid:
+ errmsg = "Invalid macro target: {}".format(errmsg)
self.perror(errmsg, traceback_war=False)
return
+ utils.unquote_redirection_tokens(args.command_args)
+
# Build the macro value string
- value = utils.quote_string_if_needed(args.command)
+ value = args.command
for cur_arg in args.command_args:
- value += ' ' + utils.quote_string_if_needed(cur_arg)
+ value += ' ' + cur_arg
# Find all normal arguments
arg_list = []
@@ -2400,8 +2449,9 @@ class Cmd(cmd.Cmd):
break
# Set the macro
+ result = "overwritten" if args.name in self.macros else "created"
self.macros[args.name] = Macro(name=args.name, value=value, required_arg_count=max_arg_num, arg_list=arg_list)
- self.poutput("Macro {!r} created".format(args.name))
+ self.poutput("Macro {!r} {}".format(args.name, result))
def macro_delete(self, args: argparse.Namespace):
""" Deletes macros """
@@ -2436,7 +2486,12 @@ class Cmd(cmd.Cmd):
self.poutput("macro create {} {}".format(cur_macro, self.macros[cur_macro].value))
# Top-level parser for macro
- macro_parser = ACArgumentParser(description="Manage macros", prog='macro')
+ macro_description = ("Manage macros\n"
+ "\n"
+ "A macro is similar to an alias, but it can take arguments when called.")
+ macro_epilog = ("See also:\n"
+ " alias")
+ macro_parser = ACArgumentParser(description=macro_description, epilog=macro_epilog, prog='macro')
# Add subcommands to macro
macro_subparsers = macro_parser.add_subparsers()
@@ -2445,41 +2500,41 @@ class Cmd(cmd.Cmd):
macro_create_help = "create or overwrite a macro"
macro_create_description = "Create or overwrite a macro"
- macro_create_epilog = "A macro is similar to an alias, but it can take arguments when called. Arguments are\n"
- macro_create_epilog += "expressed when creating a macro using {#} notation where {1} means the first argument\n"
- macro_create_epilog += "while {8} would mean the eighth.\n"
- macro_create_epilog += "\n"
- macro_create_epilog += "The following creates a macro called my_macro that expects two arguments:\n"
- macro_create_epilog += "\n"
- macro_create_epilog += "macro create my_macro make_dinner -meat {1} -veggie {2}\n"
- macro_create_epilog += "\n"
- macro_create_epilog += "When the macro is called, the provided arguments are resolved and the assembled\n"
- macro_create_epilog += "command is run. The following table shows how my_macro would be resolved.\n"
- macro_create_epilog += "\n"
- macro_create_epilog += "Macro Usage Command Run\n"
- macro_create_epilog += "my_macro beef broccoli make_dinner -meat beef -veggie broccoli\n"
- macro_create_epilog += "my_macro chicken spinach make_dinner -meat chicken -veggie spinach\n"
- macro_create_epilog += "\n"
- macro_create_epilog += "Notes:\n"
- macro_create_epilog += " To use the literal string {1} in your command, escape it this way: {{1}}\n"
- macro_create_epilog += "\n"
- macro_create_epilog += " An argument number can be repeated in a macro. In the following example the first\n"
- macro_create_epilog += " argument will populate both {1} instances\n"
- macro_create_epilog += "\n"
- macro_create_epilog += " macro create ft file_taxes -p {1} -q {2} -r {1}\n"
- macro_create_epilog += "\n"
- macro_create_epilog += " To quote an argument in the resolved command, quote it during creation:\n"
- macro_create_epilog += "\n"
- macro_create_epilog += " macro create del_file !rm -f \"{1}\"\n"
- macro_create_epilog += "\n"
- macro_create_epilog += " Macros can resolve into commands, aliases, and other macros. Therefore it is\n"
- macro_create_epilog += " possible to create a macro that results in infinite recursion if a macro ends up\n"
- macro_create_epilog += " resolving back to itself. So be careful.\n"
- macro_create_epilog += "\n"
- macro_create_epilog += " If you want to use redirection or pipes in the macro, then quote them as in the\n"
- macro_create_epilog += " following example to prevent the macro command itself from being redirected\n"
- macro_create_epilog += "\n"
- macro_create_epilog += " macro create save_results print_results -type {1} \">\" \"{2}\""
+ macro_create_epilog = ("A macro is similar to an alias, but it can take arguments when called. Arguments are\n"
+ "expressed when creating a macro using {#} notation where {1} means the first argument.\n"
+ "\n"
+ "The following creates a macro called my_macro that expects two arguments:\n"
+ "\n"
+ " macro create my_macro make_dinner -meat {1} -veggie {2}\n"
+ "\n"
+ "When the macro is called, the provided arguments are resolved and the assembled\n"
+ "command is run. For example:\n"
+ "\n"
+ " my_macro beef broccoli ---> make_dinner -meat beef -veggie broccoli\n"
+ "\n"
+ "Notes:\n"
+ " To use the literal string {1} in your command, escape it this way: {{1}}.\n"
+ "\n"
+ " An argument number can be repeated in a macro. In the following example the first\n"
+ " argument will populate both {1} instances.\n"
+ "\n"
+ " macro create ft file_taxes -p {1} -q {2} -r {1}\n"
+ "\n"
+ " To quote an argument in the resolved command, quote it during creation.\n"
+ "\n"
+ " macro create backup !cp \"{1}\" \"{1}.orig\"\n"
+ "\n"
+ " Macros can resolve into commands, aliases, and other macros. Therefore it is\n"
+ " possible to create a macro that results in infinite recursion if a macro ends up\n"
+ " resolving back to itself. So be careful.\n"
+ "\n"
+ " If you want to use redirection or pipes in the macro, then quote them as in the\n"
+ " following example to prevent the 'macro create' command itself from being redirected.\n"
+ "\n"
+ " macro create save_results print_results -type {1} \">\" \"{2}\"\n"
+ "\n"
+ " Because macros do not resolve until after parsing (hitting Enter), tab completion\n"
+ " will only complete paths.")
macro_create_parser = macro_subparsers.add_parser('create', help=macro_create_help,
description=macro_create_description,
@@ -2505,10 +2560,10 @@ class Cmd(cmd.Cmd):
# macro -> list
macro_list_help = "list macros"
- macro_list_description = "List specified macros in a reusable form that can be saved to\n"
- macro_list_description += "a startup_script to preserve macros across sessions\n"
- macro_list_description += "\n"
- macro_list_description += "Without arguments, all macros will be listed"
+ macro_list_description = ("List specified macros in a reusable form that can be saved to\n"
+ "a startup_script to preserve macros across sessions\n"
+ "\n"
+ "Without arguments, all macros will be listed.")
macro_list_parser = macro_subparsers.add_parser('list', help=macro_list_help,
description=macro_list_description)
@@ -2516,7 +2571,7 @@ class Cmd(cmd.Cmd):
ACTION_ARG_CHOICES, macros)
macro_list_parser.set_defaults(func=macro_list)
- @with_argparser(macro_parser)
+ @with_argparser(macro_parser, preserve_quotes=True)
def do_macro(self, args: argparse.Namespace):
"""Manage macros"""
func = getattr(args, 'func', None)
@@ -2766,10 +2821,10 @@ class Cmd(cmd.Cmd):
else:
raise LookupError("Parameter '{}' not supported (type 'set' for list of parameters).".format(param))
- set_description = "Set a settable parameter or show current settings of parameters.\n"
- set_description += "\n"
- set_description += "Accepts abbreviated parameter names so long as there is no ambiguity.\n"
- set_description += "Call without arguments for a list of settable parameters with their values."
+ set_description = ("Set a settable parameter or show current settings of parameters.\n"
+ "\n"
+ "Accepts abbreviated parameter names so long as there is no ambiguity.\n"
+ "Call without arguments for a list of settable parameters with their values.")
set_parser = ACArgumentParser(description=set_description)
set_parser.add_argument('-a', '--all', action='store_true', help='display read-only settings as well')
@@ -2780,7 +2835,7 @@ class Cmd(cmd.Cmd):
@with_argparser(set_parser)
def do_set(self, args: argparse.Namespace) -> None:
- """Set a settable parameter or shows current settings of parameters"""
+ """Set a settable parameter or show current settings of parameters"""
# Check if param was passed in
if not args.param:
diff --git a/cmd2/constants.py b/cmd2/constants.py
index d3e8a125..3c133b70 100644
--- a/cmd2/constants.py
+++ b/cmd2/constants.py
@@ -17,3 +17,8 @@ REDIRECTION_TOKENS = [REDIRECTION_PIPE, REDIRECTION_OUTPUT, REDIRECTION_APPEND]
ANSI_ESCAPE_RE = re.compile(r'\x1b[^m]*m')
LINE_FEED = '\n'
+
+# values for colors setting
+COLORS_NEVER = 'Never'
+COLORS_TERMINAL = 'Terminal'
+COLORS_ALWAYS = 'Always'
diff --git a/cmd2/parsing.py b/cmd2/parsing.py
index 498834cf..82e8ee39 100644
--- a/cmd2/parsing.py
+++ b/cmd2/parsing.py
@@ -291,7 +291,7 @@ class StatementParser:
expr = r'\A\s*(\S*?)({})'.format(second_group)
self._command_pattern = re.compile(expr)
- def is_valid_command(self, word: str) -> Tuple[bool, str]:
+ def is_valid_command(self, word: str, allow_shortcut: bool) -> Tuple[bool, str]:
"""Determine whether a word is a valid name for a command.
Commands can not include redirection characters, whitespace,
@@ -311,11 +311,12 @@ class StatementParser:
if not word:
return False, 'cannot be an empty string'
- errmsg = 'cannot start with a shortcut: '
- errmsg += ', '.join(shortcut for (shortcut, expansion) in self.shortcuts)
- for (shortcut, expansion) in self.shortcuts:
- if word.startswith(shortcut):
- return False, errmsg
+ if not allow_shortcut:
+ errmsg = 'cannot start with a shortcut: '
+ errmsg += ', '.join(shortcut for (shortcut, expansion) in self.shortcuts)
+ for (shortcut, expansion) in self.shortcuts:
+ if word.startswith(shortcut):
+ return False, errmsg
errmsg = 'cannot contain: whitespace, quotes, '
errchars = []
diff --git a/cmd2/utils.py b/cmd2/utils.py
index 3527236f..a20f0b66 100644
--- a/cmd2/utils.py
+++ b/cmd2/utils.py
@@ -304,3 +304,15 @@ class StdSim(object):
return self.__dict__[item]
else:
return getattr(self.inner_stream, item)
+
+
+def unquote_redirection_tokens(args: List[str]) -> None:
+ """
+ Used to unquote redirection tokens in a list of command line arguments
+ This is used when redirection tokens have to be passed to another command
+ :param args: the command line args
+ """
+ for i, arg in enumerate(args):
+ unquoted_arg = strip_quotes(arg)
+ if unquoted_arg in constants.REDIRECTION_TOKENS:
+ args[i] = unquoted_arg
diff --git a/docs/settingchanges.rst b/docs/settingchanges.rst
index 02955273..e08b6026 100644
--- a/docs/settingchanges.rst
+++ b/docs/settingchanges.rst
@@ -137,7 +137,7 @@ comments, is viewable from within a running application
with::
(Cmd) set --long
- colors: True # Colorized output (*nix only)
+ colors: Terminal # Allow colorized output
continuation_prompt: > # On 2nd+ line of input
debug: False # Show full error stack on error
echo: False # Echo command issued into output
@@ -150,5 +150,5 @@ with::
Any of these user-settable parameters can be set while running your app with the ``set`` command like so::
- set colors False
+ set colors Never
diff --git a/docs/unfreefeatures.rst b/docs/unfreefeatures.rst
index b5f9415d..364addc6 100644
--- a/docs/unfreefeatures.rst
+++ b/docs/unfreefeatures.rst
@@ -139,23 +139,43 @@ instead. These methods have these advantages:
.. automethod:: cmd2.cmd2.Cmd.ppaged
-color
-=====
+Colored Output
+==============
-Text output can be colored by wrapping it in the ``colorize`` method.
+The output methods in the previous section all honor the ``colors`` setting,
+which has three possible values:
+
+Never
+ poutput() and pfeedback() strip all ANSI escape sequences
+ which instruct the terminal to colorize output
+
+Terminal
+ (the default value) poutput() and pfeedback() do not strip any ANSI escape
+ sequences when the output is a terminal, but if the output is a pipe or a
+ file the escape sequences are stripped. If you want colorized output you
+ must add ANSI escape sequences, preferably using some python color library
+ like `plumbum.colors`, `colorama`, `blessings`, or `termcolor`.
+
+Always
+ poutput() and pfeedback() never strip ANSI escape sequences, regardless of
+ the output destination
+
+
+The previously recommended ``colorize`` method is now deprecated.
-.. automethod:: cmd2.cmd2.Cmd.colorize
.. _quiet:
+Suppressing non-essential output
+================================
-quiet
-=====
+The ``quiet`` setting controls whether ``self.pfeedback()`` actually produces
+any output. If ``quiet`` is ``False``, then the output will be produced. If
+``quiet`` is ``True``, no output will be produced.
-Controls whether ``self.pfeedback('message')`` output is suppressed;
-useful for non-essential feedback that the user may not always want
-to read. ``quiet`` is only relevant if
-``app.pfeedback`` is sometimes used.
+This makes ``self.pfeedback()`` useful for non-essential output like status
+messages. Users can control whether they would like to see these messages by changing
+the value of the ``quiet`` setting.
select
diff --git a/examples/colors.py b/examples/colors.py
new file mode 100755
index 00000000..8765aee0
--- /dev/null
+++ b/examples/colors.py
@@ -0,0 +1,142 @@
+#!/usr/bin/env python
+# coding=utf-8
+"""
+A sample application for cmd2. Demonstrating colorized output.
+
+Experiment with the command line options on the `speak` command to see how
+different output colors ca
+
+The colors setting has three possible values:
+
+Never
+ poutput() and pfeedback() strip all ANSI escape sequences
+ which instruct the terminal to colorize output
+
+Terminal
+ (the default value) poutput() and pfeedback() do not strip any ANSI escape
+ sequences when the output is a terminal, but if the output is a pipe or a
+ file the escape sequences are stripped. If you want colorized output you
+ must add ANSI escape sequences, preferably using some python color library
+ like `plumbum.colors`, `colorama`, `blessings`, or `termcolor`.
+
+Always
+ poutput() and pfeedback() never strip ANSI escape sequences, regardless of
+ the output destination
+"""
+
+import random
+import argparse
+
+import cmd2
+from colorama import Fore, Back
+
+FG_COLORS = {
+ 'black': Fore.BLACK,
+ 'red': Fore.RED,
+ 'green': Fore.GREEN,
+ 'yellow': Fore.YELLOW,
+ 'blue': Fore.BLUE,
+ 'magenta': Fore.MAGENTA,
+ 'cyan': Fore.CYAN,
+ 'white': Fore.WHITE,
+}
+BG_COLORS = {
+ 'black': Back.BLACK,
+ 'red': Back.RED,
+ 'green': Back.GREEN,
+ 'yellow': Back.YELLOW,
+ 'blue': Back.BLUE,
+ 'magenta': Back.MAGENTA,
+ 'cyan': Back.CYAN,
+ 'white': Back.WHITE,
+}
+
+
+class CmdLineApp(cmd2.Cmd):
+ """Example cmd2 application demonstrating colorized output."""
+
+ # Setting this true makes it run a shell command if a cmd2/cmd command doesn't exist
+ # default_to_shell = True
+ MUMBLES = ['like', '...', 'um', 'er', 'hmmm', 'ahh']
+ MUMBLE_FIRST = ['so', 'like', 'well']
+ MUMBLE_LAST = ['right?']
+
+ def __init__(self):
+ self.multiline_commands = ['orate']
+ self.maxrepeats = 3
+
+ # Add stuff to settable and shortcuts before calling base class initializer
+ self.settable['maxrepeats'] = 'max repetitions for speak command'
+ self.shortcuts.update({'&': 'speak'})
+
+ # Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell
+ super().__init__(use_ipython=True)
+
+ speak_parser = argparse.ArgumentParser()
+ speak_parser.add_argument('-p', '--piglatin', action='store_true', help='atinLay')
+ speak_parser.add_argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE')
+ speak_parser.add_argument('-r', '--repeat', type=int, help='output [n] times')
+ speak_parser.add_argument('-f', '--fg', choices=FG_COLORS, help='foreground color to apply to output')
+ speak_parser.add_argument('-b', '--bg', choices=BG_COLORS, help='background color to apply to output')
+ speak_parser.add_argument('words', nargs='+', help='words to say')
+
+ @cmd2.with_argparser(speak_parser)
+ def do_speak(self, args):
+ """Repeats what you tell me to."""
+ words = []
+ for word in args.words:
+ if args.piglatin:
+ word = '%s%say' % (word[1:], word[0])
+ if args.shout:
+ word = word.upper()
+ words.append(word)
+
+ repetitions = args.repeat or 1
+
+ color_on = ''
+ if args.fg:
+ color_on += FG_COLORS[args.fg]
+ if args.bg:
+ color_on += BG_COLORS[args.bg]
+ color_off = Fore.RESET + Back.RESET
+
+ for i in range(min(repetitions, self.maxrepeats)):
+ # .poutput handles newlines, and accommodates output redirection too
+ self.poutput(color_on + ' '.join(words) + color_off)
+
+ do_say = do_speak # now "say" is a synonym for "speak"
+ do_orate = do_speak # another synonym, but this one takes multi-line input
+
+ mumble_parser = argparse.ArgumentParser()
+ mumble_parser.add_argument('-r', '--repeat', type=int, help='how many times to repeat')
+ mumble_parser.add_argument('-f', '--fg', help='foreground color to apply to output')
+ mumble_parser.add_argument('-b', '--bg', help='background color to apply to output')
+ mumble_parser.add_argument('words', nargs='+', help='words to say')
+
+ @cmd2.with_argparser(mumble_parser)
+ def do_mumble(self, args):
+ """Mumbles what you tell me to."""
+ color_on = ''
+ if args.fg and args.fg in FG_COLORS:
+ color_on += FG_COLORS[args.fg]
+ if args.bg and args.bg in BG_COLORS:
+ color_on += BG_COLORS[args.bg]
+ color_off = Fore.RESET + Back.RESET
+
+ repetitions = args.repeat or 1
+ for i in range(min(repetitions, self.maxrepeats)):
+ output = []
+ if random.random() < .33:
+ output.append(random.choice(self.MUMBLE_FIRST))
+ for word in args.words:
+ if random.random() < .40:
+ output.append(random.choice(self.MUMBLES))
+ output.append(word)
+ if random.random() < .25:
+ output.append(random.choice(self.MUMBLE_LAST))
+ self.poutput(color_on + ' '.join(output) + color_off)
+
+
+if __name__ == '__main__':
+ c = CmdLineApp()
+ c.cmdloop()
diff --git a/examples/pirate.py b/examples/pirate.py
index 34906a9f..22274dbf 100755
--- a/examples/pirate.py
+++ b/examples/pirate.py
@@ -8,8 +8,21 @@ It demonstrates many features of cmd2.
"""
import argparse
+from colorama import Fore
+
import cmd2
+COLORS = {
+ 'black': Fore.BLACK,
+ 'red': Fore.RED,
+ 'green': Fore.GREEN,
+ 'yellow': Fore.YELLOW,
+ 'blue': Fore.BLUE,
+ 'magenta': Fore.MAGENTA,
+ 'cyan': Fore.CYAN,
+ 'white': Fore.WHITE,
+}
+
class Pirate(cmd2.Cmd):
"""A piratical example cmd2 application involving looting and drinking."""
@@ -17,10 +30,10 @@ class Pirate(cmd2.Cmd):
self.default_to_shell = True
self.multiline_commands = ['sing']
self.terminators = self.terminators + ['...']
- self.songcolor = 'blue'
+ self.songcolor = Fore.BLUE
# Add stuff to settable and/or shortcuts before calling base class initializer
- self.settable['songcolor'] = 'Color to ``sing`` in (red/blue/green/cyan/magenta, bold, underline)'
+ self.settable['songcolor'] = 'Color to ``sing`` in (black/red/green/yellow/blue/magenta/cyan/white)'
self.shortcuts.update({'~': 'sing'})
"""Initialize the base class as well as this one"""
@@ -68,7 +81,8 @@ class Pirate(cmd2.Cmd):
def do_sing(self, arg):
"""Sing a colorful song."""
- self.poutput(self.colorize(arg, self.songcolor))
+ color_escape = COLORS.get(self.songcolor, default=Fore.RESET)
+ self.poutput(arg, color=color_escape)
yo_parser = argparse.ArgumentParser()
yo_parser.add_argument('--ho', type=int, default=2, help="How often to chant 'ho'")
diff --git a/examples/plumbum_colors.py b/examples/plumbum_colors.py
new file mode 100755
index 00000000..942eaf80
--- /dev/null
+++ b/examples/plumbum_colors.py
@@ -0,0 +1,144 @@
+#!/usr/bin/env python
+# coding=utf-8
+"""
+A sample application for cmd2. Demonstrating colorized output using the plumbum package.
+
+Experiment with the command line options on the `speak` command to see how
+different output colors ca
+
+The colors setting has three possible values:
+
+Never
+ poutput() and pfeedback() strip all ANSI escape sequences
+ which instruct the terminal to colorize output
+
+Terminal
+ (the default value) poutput() and pfeedback() do not strip any ANSI escape
+ sequences when the output is a terminal, but if the output is a pipe or a
+ file the escape sequences are stripped. If you want colorized output you
+ must add ANSI escape sequences, preferably using some python color library
+ like `plumbum.colors`, `colorama`, `blessings`, or `termcolor`.
+
+Always
+ poutput() and pfeedback() never strip ANSI escape sequences, regardless of
+ the output destination
+
+WARNING: This example requires the plumbum package, which isn't normally required by cmd2.
+"""
+
+import random
+import argparse
+
+import cmd2
+from plumbum.colors import fg, bg, reset
+
+FG_COLORS = {
+ 'black': fg.Black,
+ 'red': fg.DarkRedA,
+ 'green': fg.MediumSpringGreen,
+ 'yellow': fg.LightYellow,
+ 'blue': fg.RoyalBlue1,
+ 'magenta': fg.Purple,
+ 'cyan': fg.SkyBlue1,
+ 'white': fg.White,
+}
+BG_COLORS = {
+ 'black': bg.BLACK,
+ 'red': bg.DarkRedA,
+ 'green': bg.MediumSpringGreen,
+ 'yellow': bg.LightYellow,
+ 'blue': bg.RoyalBlue1,
+ 'magenta': bg.Purple,
+ 'cyan': bg.SkyBlue1,
+ 'white': bg.White,
+}
+
+
+class CmdLineApp(cmd2.Cmd):
+ """Example cmd2 application demonstrating colorized output."""
+
+ # Setting this true makes it run a shell command if a cmd2/cmd command doesn't exist
+ # default_to_shell = True
+ MUMBLES = ['like', '...', 'um', 'er', 'hmmm', 'ahh']
+ MUMBLE_FIRST = ['so', 'like', 'well']
+ MUMBLE_LAST = ['right?']
+
+ def __init__(self):
+ self.multiline_commands = ['orate']
+ self.maxrepeats = 3
+
+ # Add stuff to settable and shortcuts before calling base class initializer
+ self.settable['maxrepeats'] = 'max repetitions for speak command'
+ self.shortcuts.update({'&': 'speak'})
+
+ # Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell
+ super().__init__(use_ipython=True)
+
+ speak_parser = argparse.ArgumentParser()
+ speak_parser.add_argument('-p', '--piglatin', action='store_true', help='atinLay')
+ speak_parser.add_argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE')
+ speak_parser.add_argument('-r', '--repeat', type=int, help='output [n] times')
+ speak_parser.add_argument('-f', '--fg', choices=FG_COLORS, help='foreground color to apply to output')
+ speak_parser.add_argument('-b', '--bg', choices=BG_COLORS, help='background color to apply to output')
+ speak_parser.add_argument('words', nargs='+', help='words to say')
+
+ @cmd2.with_argparser(speak_parser)
+ def do_speak(self, args):
+ """Repeats what you tell me to."""
+ words = []
+ for word in args.words:
+ if args.piglatin:
+ word = '%s%say' % (word[1:], word[0])
+ if args.shout:
+ word = word.upper()
+ words.append(word)
+
+ repetitions = args.repeat or 1
+
+ color_on = ''
+ if args.fg:
+ color_on += FG_COLORS[args.fg]
+ if args.bg:
+ color_on += BG_COLORS[args.bg]
+ color_off = reset
+
+ for i in range(min(repetitions, self.maxrepeats)):
+ # .poutput handles newlines, and accommodates output redirection too
+ self.poutput(color_on + ' '.join(words) + color_off)
+
+ do_say = do_speak # now "say" is a synonym for "speak"
+ do_orate = do_speak # another synonym, but this one takes multi-line input
+
+ mumble_parser = argparse.ArgumentParser()
+ mumble_parser.add_argument('-r', '--repeat', type=int, help='how many times to repeat')
+ mumble_parser.add_argument('-f', '--fg', help='foreground color to apply to output')
+ mumble_parser.add_argument('-b', '--bg', help='background color to apply to output')
+ mumble_parser.add_argument('words', nargs='+', help='words to say')
+
+ @cmd2.with_argparser(mumble_parser)
+ def do_mumble(self, args):
+ """Mumbles what you tell me to."""
+ color_on = ''
+ if args.fg and args.fg in FG_COLORS:
+ color_on += FG_COLORS[args.fg]
+ if args.bg and args.bg in BG_COLORS:
+ color_on += BG_COLORS[args.bg]
+ color_off = Fore.RESET + Back.RESET
+
+ repetitions = args.repeat or 1
+ for i in range(min(repetitions, self.maxrepeats)):
+ output = []
+ if random.random() < .33:
+ output.append(random.choice(self.MUMBLE_FIRST))
+ for word in args.words:
+ if random.random() < .40:
+ output.append(random.choice(self.MUMBLES))
+ output.append(word)
+ if random.random() < .25:
+ output.append(random.choice(self.MUMBLE_LAST))
+ self.poutput(color_on + ' '.join(output) + color_off)
+
+
+if __name__ == '__main__':
+ c = CmdLineApp()
+ c.cmdloop()
diff --git a/examples/python_scripting.py b/examples/python_scripting.py
index 4c959f58..0b0030a5 100755
--- a/examples/python_scripting.py
+++ b/examples/python_scripting.py
@@ -17,6 +17,8 @@ This application and the "scripts/conditional.py" script serve as an example for
import argparse
import os
+from colorama import Fore
+
import cmd2
@@ -33,7 +35,7 @@ class CmdLineApp(cmd2.Cmd):
def _set_prompt(self):
"""Set prompt so it displays the current working directory."""
self.cwd = os.getcwd()
- self.prompt = self.colorize('{!r} $ '.format(self.cwd), 'cyan')
+ self.prompt = Fore.CYAN + '{!r} $ '.format(self.cwd) + Fore.RESET
def postcmd(self, stop: bool, line: str) -> bool:
"""Hook method executed just after a command dispatch is finished.
diff --git a/examples/transcripts/exampleSession.txt b/examples/transcripts/exampleSession.txt
index 6318776f..38fb0659 100644
--- a/examples/transcripts/exampleSession.txt
+++ b/examples/transcripts/exampleSession.txt
@@ -3,7 +3,7 @@
# The regex for editor will match whatever program you use.
# regexes on prompts just make the trailing space obvious
(Cmd) set
-colors: /(True|False)/
+colors: /(Terminal|Always|Never)/
continuation_prompt: >/ /
debug: False
echo: False
diff --git a/examples/transcripts/transcript_regex.txt b/examples/transcripts/transcript_regex.txt
index 08588ab1..6980fac6 100644
--- a/examples/transcripts/transcript_regex.txt
+++ b/examples/transcripts/transcript_regex.txt
@@ -3,7 +3,7 @@
# The regex for editor will match whatever program you use.
# regexes on prompts just make the trailing space obvious
(Cmd) set
-colors: /(True|False)/
+colors: /(Terminal|Always|Never)/
continuation_prompt: >/ /
debug: False
echo: False
diff --git a/tests/conftest.py b/tests/conftest.py
index 1295a633..da6ffcd6 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -45,7 +45,7 @@ macro Manage macros
py Invoke python command or shell
pyscript Run a python script file inside the console
quit Exit this application
-set Set a settable parameter or shows current settings of parameters
+set Set a settable parameter or show current settings of parameters
shell Execute a command as if at the OS prompt
shortcuts List shortcuts available
"""
@@ -82,11 +82,8 @@ SHORTCUTS_TXT = """Shortcuts for other commands:
@@: _relative_load
"""
-expect_colors = True
-if sys.platform.startswith('win'):
- expect_colors = False
# Output from the show command with default settings
-SHOW_TXT = """colors: {}
+SHOW_TXT = """colors: Terminal
continuation_prompt: >
debug: False
echo: False
@@ -96,14 +93,10 @@ locals_in_py: False
prompt: (Cmd)
quiet: False
timing: False
-""".format(expect_colors)
+"""
-if expect_colors:
- color_str = 'True '
-else:
- color_str = 'False'
SHOW_LONG = """
-colors: {} # Colorized output (*nix only)
+colors: Terminal # Allow colorized output (valid values: Terminal, Always, Never)
continuation_prompt: > # On 2nd+ line of input
debug: False # Show full error stack on error
echo: False # Echo command issued into output
@@ -113,8 +106,7 @@ locals_in_py: False # Allow access to your application in py via self
prompt: (Cmd) # The prompt issued to solicit input
quiet: False # Don't print nonessential feedback
timing: False # Report execution times
-""".format(color_str)
-
+"""
def normalize(block):
""" Normalize a block of text to perform comparison.
diff --git a/tests/scripts/postcmds.txt b/tests/scripts/postcmds.txt
index 2b478b57..dea8f265 100644
--- a/tests/scripts/postcmds.txt
+++ b/tests/scripts/postcmds.txt
@@ -1 +1 @@
-set colors off
+set colors Never
diff --git a/tests/scripts/precmds.txt b/tests/scripts/precmds.txt
index d0b27fb6..0ae7eae8 100644
--- a/tests/scripts/precmds.txt
+++ b/tests/scripts/precmds.txt
@@ -1 +1 @@
-set colors on
+set colors Always
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index d250af26..d3d1d585 100644
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -13,6 +13,7 @@ import os
import sys
import tempfile
+from colorama import Fore, Back, Style
import pytest
# Python 3.5 had some regressions in the unitest.mock module, so use 3rd party mock if available
@@ -564,11 +565,11 @@ def test_load_nested_loads(base_app, request):
expected = """
%s
_relative_load precmds.txt
-set colors on
+set colors Always
help
shortcuts
_relative_load postcmds.txt
-set colors off""" % initial_load
+set colors Never""" % initial_load
assert run_cmd(base_app, 'history -s') == normalize(expected)
@@ -586,11 +587,11 @@ def test_base_runcmds_plus_hooks(base_app, request):
'load ' + postfilepath])
expected = """
load %s
-set colors on
+set colors Always
help
shortcuts
load %s
-set colors off""" % (prefilepath, postfilepath)
+set colors Never""" % (prefilepath, postfilepath)
assert run_cmd(base_app, 'history -s') == normalize(expected)
@@ -817,12 +818,7 @@ def test_base_colorize(base_app):
# But if we create a fresh Cmd() instance, it will
fresh_app = cmd2.Cmd()
color_test = fresh_app.colorize('Test', 'red')
- # Actually, colorization only ANSI escape codes is only applied on non-Windows systems
- if sys.platform == 'win32':
- assert color_test == 'Test'
- else:
- assert color_test == '\x1b[31mTest\x1b[39m'
-
+ assert color_test == '\x1b[31mTest\x1b[39m'
def _expected_no_editor_error():
expected_exception = 'OSError'
@@ -1111,22 +1107,22 @@ def test_ansi_prompt_not_esacped(base_app):
def test_ansi_prompt_escaped():
from cmd2.rl_utils import rl_make_safe_prompt
app = cmd2.Cmd()
- color = 'cyan'
+ color = Fore.CYAN
prompt = 'InColor'
- color_prompt = app.colorize(prompt, color)
+ color_prompt = color + prompt + Fore.RESET
readline_hack_start = "\x01"
readline_hack_end = "\x02"
readline_safe_prompt = rl_make_safe_prompt(color_prompt)
+ assert prompt != color_prompt
if sys.platform.startswith('win'):
- # colorize() does nothing on Windows due to lack of ANSI color support
- assert prompt == color_prompt
- assert readline_safe_prompt == prompt
+ # PyReadline on Windows doesn't suffer from the GNU readline bug which requires the hack
+ assert readline_safe_prompt.startswith(color)
+ assert readline_safe_prompt.endswith(Fore.RESET)
else:
- assert prompt != color_prompt
- assert readline_safe_prompt.startswith(readline_hack_start + app._colorcodes[color][True] + readline_hack_end)
- assert readline_safe_prompt.endswith(readline_hack_start + app._colorcodes[color][False] + readline_hack_end)
+ assert readline_safe_prompt.startswith(readline_hack_start + color + readline_hack_end)
+ assert readline_safe_prompt.endswith(readline_hack_start + Fore.RESET + readline_hack_end)
class HelpApp(cmd2.Cmd):
@@ -1263,7 +1259,7 @@ macro Manage macros
py Invoke python command or shell
pyscript Run a python script file inside the console
quit Exit this application
-set Set a settable parameter or shows current settings of parameters
+set Set a settable parameter or show current settings of parameters
shell Execute a command as if at the OS prompt
shortcuts List shortcuts available
@@ -1752,6 +1748,24 @@ def test_poutput_none(base_app):
expected = ''
assert out == expected
+def test_poutput_color_always(base_app):
+ msg = 'Hello World'
+ color = Fore.CYAN
+ base_app.colors = 'Always'
+ base_app.poutput(msg, color=color)
+ out = base_app.stdout.getvalue()
+ expected = color + msg + '\n' + Fore.RESET
+ assert out == expected
+
+def test_poutput_color_never(base_app):
+ msg = 'Hello World'
+ color = Fore.CYAN
+ base_app.colors = 'Never'
+ base_app.poutput(msg, color=color)
+ out = base_app.stdout.getvalue()
+ expected = msg + '\n'
+ assert out == expected
+
def test_alias_create(base_app, capsys):
# Create the alias
@@ -1767,7 +1781,7 @@ def test_alias_create(base_app, capsys):
out = run_cmd(base_app, 'alias list')
assert out == normalize('alias create fake pyscript')
- # Lookup the new alias
+ # Look up the new alias
out = run_cmd(base_app, 'alias list fake')
assert out == normalize('alias create fake pyscript')
@@ -1776,7 +1790,7 @@ def test_alias_create_with_quotes(base_app, capsys):
out = run_cmd(base_app, 'alias create fake help ">" "out file.txt"')
assert out == normalize("Alias 'fake' created")
- # Lookup the new alias (Only the redirector should be unquoted)
+ # Look up the new alias (Only the redirector should be unquoted)
out = run_cmd(base_app, 'alias list fake')
assert out == normalize('alias create fake help > "out file.txt"')
@@ -1793,7 +1807,7 @@ def test_alias_create_with_quotes(base_app, capsys):
def test_alias_create_invalid_name(base_app, alias_name, capsys):
run_cmd(base_app, 'alias create {} help'.format(alias_name))
out, err = capsys.readouterr()
- assert "cannot" in err
+ assert "Invalid alias name" in err
def test_alias_create_with_macro_name(base_app, capsys):
macro = "my_macro"
@@ -1802,13 +1816,22 @@ def test_alias_create_with_macro_name(base_app, capsys):
out, err = capsys.readouterr()
assert "cannot have the same name" in err
-def test_alias_create_with_empty_command(base_app, capsys):
- run_cmd(base_app, 'alias create my_alias ""')
+@pytest.mark.parametrize('alias_target', [
+ '""', # Blank name
+ '">"',
+ '"no>pe"',
+ '"no spaces"',
+ '"nopipe|"',
+ '"noterm;"',
+ 'noembedded"quotes',
+])
+def test_alias_create_with_invalid_command(base_app, alias_target, capsys):
+ run_cmd(base_app, 'alias create my_alias {}'.format(alias_target))
out, err = capsys.readouterr()
- assert "cannot resolve to an empty command" in err
+ assert "Invalid alias target" in err
def test_alias_list_invalid_alias(base_app, capsys):
- # Lookup invalid alias
+ # Look up invalid alias
out = run_cmd(base_app, 'alias list invalid')
out, err = capsys.readouterr()
assert "not found" in err
@@ -1965,7 +1988,6 @@ def test_bad_history_file_path(capsys, request):
assert 'readline cannot read' in err
-
def test_get_all_commands(base_app):
# Verify that the base app has the expected commands
commands = base_app.get_all_commands()
@@ -2052,3 +2074,136 @@ def test_exit_code_nonzero(exit_code_repl):
app.cmdloop()
out = app.stdout.getvalue()
assert out == expected
+
+
+class ColorsApp(cmd2.Cmd):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ def do_echo(self, args):
+ self.poutput(args)
+ self.perror(args, False)
+
+ def do_echo_error(self, args):
+ color_on = Fore.RED + Back.BLACK
+ color_off = Style.RESET_ALL
+ self.poutput(color_on + args + color_off)
+ # perror uses colors by default
+ self.perror(args, False)
+
+def test_colors_default():
+ app = ColorsApp()
+ assert app.colors == cmd2.constants.COLORS_TERMINAL
+
+def test_colors_pouterr_always_tty(mocker, capsys):
+ app = ColorsApp()
+ app.colors = cmd2.constants.COLORS_ALWAYS
+ mocker.patch.object(app.stdout, 'isatty', return_value=True)
+ mocker.patch.object(sys.stderr, 'isatty', return_value=True)
+
+ app.onecmd_plus_hooks('echo_error oopsie')
+ out, err = capsys.readouterr()
+ # if colors are on, the output should have some escape sequences in it
+ assert len(out) > len('oopsie\n')
+ assert 'oopsie' in out
+ assert len(err) > len('Error: oopsie\n')
+ assert 'ERROR: oopsie' in err
+
+ # but this one shouldn't
+ app.onecmd_plus_hooks('echo oopsie')
+ out, err = capsys.readouterr()
+ assert out == 'oopsie\n'
+ # errors always have colors
+ assert len(err) > len('Error: oopsie\n')
+ assert 'ERROR: oopsie' in err
+
+def test_colors_pouterr_always_notty(mocker, capsys):
+ app = ColorsApp()
+ app.colors = cmd2.constants.COLORS_ALWAYS
+ mocker.patch.object(app.stdout, 'isatty', return_value=False)
+ mocker.patch.object(sys.stderr, 'isatty', return_value=False)
+
+ app.onecmd_plus_hooks('echo_error oopsie')
+ out, err = capsys.readouterr()
+ # if colors are on, the output should have some escape sequences in it
+ assert len(out) > len('oopsie\n')
+ assert 'oopsie' in out
+ assert len(err) > len('Error: oopsie\n')
+ assert 'ERROR: oopsie' in err
+
+ # but this one shouldn't
+ app.onecmd_plus_hooks('echo oopsie')
+ out, err = capsys.readouterr()
+ assert out == 'oopsie\n'
+ # errors always have colors
+ assert len(err) > len('Error: oopsie\n')
+ assert 'ERROR: oopsie' in err
+
+def test_colors_terminal_tty(mocker, capsys):
+ app = ColorsApp()
+ app.colors = cmd2.constants.COLORS_TERMINAL
+ mocker.patch.object(app.stdout, 'isatty', return_value=True)
+ mocker.patch.object(sys.stderr, 'isatty', return_value=True)
+
+ app.onecmd_plus_hooks('echo_error oopsie')
+ # if colors are on, the output should have some escape sequences in it
+ out, err = capsys.readouterr()
+ assert len(out) > len('oopsie\n')
+ assert 'oopsie' in out
+ assert len(err) > len('Error: oopsie\n')
+ assert 'ERROR: oopsie' in err
+
+ # but this one shouldn't
+ app.onecmd_plus_hooks('echo oopsie')
+ out, err = capsys.readouterr()
+ assert out == 'oopsie\n'
+ assert len(err) > len('Error: oopsie\n')
+ assert 'ERROR: oopsie' in err
+
+def test_colors_terminal_notty(mocker, capsys):
+ app = ColorsApp()
+ app.colors = cmd2.constants.COLORS_TERMINAL
+ mocker.patch.object(app.stdout, 'isatty', return_value=False)
+ mocker.patch.object(sys.stderr, 'isatty', return_value=False)
+
+ app.onecmd_plus_hooks('echo_error oopsie')
+ out, err = capsys.readouterr()
+ assert out == 'oopsie\n'
+ assert err == 'ERROR: oopsie\n'
+
+ app.onecmd_plus_hooks('echo oopsie')
+ out, err = capsys.readouterr()
+ assert out == 'oopsie\n'
+ assert err == 'ERROR: oopsie\n'
+
+def test_colors_never_tty(mocker, capsys):
+ app = ColorsApp()
+ app.colors = cmd2.constants.COLORS_NEVER
+ mocker.patch.object(app.stdout, 'isatty', return_value=True)
+ mocker.patch.object(sys.stderr, 'isatty', return_value=True)
+
+ app.onecmd_plus_hooks('echo_error oopsie')
+ out, err = capsys.readouterr()
+ assert out == 'oopsie\n'
+ assert err == 'ERROR: oopsie\n'
+
+ app.onecmd_plus_hooks('echo oopsie')
+ out, err = capsys.readouterr()
+ assert out == 'oopsie\n'
+ assert err == 'ERROR: oopsie\n'
+
+def test_colors_never_notty(mocker, capsys):
+ app = ColorsApp()
+ app.colors = cmd2.constants.COLORS_NEVER
+ mocker.patch.object(app.stdout, 'isatty', return_value=False)
+ mocker.patch.object(sys.stderr, 'isatty', return_value=False)
+
+ app.onecmd_plus_hooks('echo_error oopsie')
+ out, err = capsys.readouterr()
+ assert out == 'oopsie\n'
+ assert err == 'ERROR: oopsie\n'
+
+ app.onecmd_plus_hooks('echo oopsie')
+ out, err = capsys.readouterr()
+ assert out == 'oopsie\n'
+ assert err == 'ERROR: oopsie\n'
diff --git a/tests/transcripts/regex_set.txt b/tests/transcripts/regex_set.txt
index b818c464..d45672a7 100644
--- a/tests/transcripts/regex_set.txt
+++ b/tests/transcripts/regex_set.txt
@@ -1,10 +1,10 @@
# Run this transcript with "python example.py -t transcript_regex.txt"
-# The regex for colors is because no color on Windows.
+# The regex for colors shows all possible settings for colors
# The regex for editor will match whatever program you use.
# Regexes on prompts just make the trailing space obvious
(Cmd) set
-colors: /(True|False)/
+colors: /(Terminal|Always|Never)/
continuation_prompt: >/ /
debug: False
echo: False