Skip to content

Commit d9ec6f7

Browse files
authored
Merge pull request #704 from python-cmd2/feature/revisit-color-support
#698 - Revisiting Color Support
2 parents bef0774 + 83e4188 commit d9ec6f7

28 files changed

+703
-509
lines changed

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,33 @@
22
* Enhancements
33
* Added support for and testing with Python 3.8, starting with 3.8 beta
44
* Improved information displayed during transcript testing
5+
* Added `ansi` module with functions and constants to support ANSI escape sequences which are used for things
6+
like applying style to text
7+
* Added support for applying styles (color, bold, underline) to text via `style()` function in `ansi` module
8+
* Added default styles to ansi.py for printing `success`, `warning`. and `error` text. These are the styles used
9+
by cmd2 and can be overridden to match the color scheme of your application.
10+
* Added `ansi_aware_write()` function to `ansi` module. This function takes into account the value of `allow_ansi`
11+
to determine if ANSI escape sequences should be stripped when not writing to a tty. See documentation for more
12+
information on the `allow_ansi` setting.
513
* Breaking Changes
614
* Python 3.4 reached its [end of life](https://www.python.org/dev/peps/pep-0429/) on March 18, 2019 and is no longer supported by `cmd2`
715
* If you need to use Python 3.4, you should pin your requirements to use `cmd2` 0.9.13
816
* Made lots of changes to minimize the public API of the `cmd2.Cmd` class
917
* Attributes and methods we do not intend to be public now all begin with an underscore
1018
* We make no API stability guarantees about these internal functions
19+
* Split `perror` into 2 functions:
20+
* `perror` - print a message to sys.stderr
21+
* `pexcept` - print Exception message to sys.stderr. If debug is true, print exception traceback if one exists
22+
* Signature of `poutput` and `perror` significantly changed
23+
* Removed color parameters `color`, `err_color`, and `war_color` from `poutput` and `perror`
24+
* See the docstrings of these methods or the [cmd2 docs](https://cmd2.readthedocs.io/en/latest/unfreefeatures.html#poutput-pfeedback-perror-ppaged) for more info on applying styles to output messages
25+
* `end` argument is now keyword-only and cannot be specified positionally
26+
* `traceback_war` no longer exists as an argument since it isn't needed now that `perror` and `pexcept` exist
27+
* Moved `cmd2.Cmd.colors` to ansi.py and renamed it to `allow_ansi`. This is now an application-wide setting.
28+
* Renamed the following constants and moved them to ansi.py
29+
* `COLORS_ALWAYS` --> `ANSI_ALWAYS`
30+
* `COLORS_NEVER` --> `ANSI_NEVER`
31+
* `COLORS_TERMINAL` --> `ANSI_TERMINAL`
1132
* **Renamed Commands Notice**
1233
* The following commands have been renamed. The old names will be supported until the next release.
1334
* `load` --> `run_script`

README.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ Main Features
4343
- Built-in regression testing framework for your applications (transcript-based testing)
4444
- Transcripts for use with built-in regression can be automatically generated from `history -t` or `run_script -t`
4545
- Alerts that seamlessly print while user enters text at prompt
46+
- Colored and stylized output using `ansi.style()`
4647

4748
Python 2.7 support is EOL
4849
-------------------------
@@ -89,7 +90,7 @@ Instructions for implementing each feature follow.
8990
class MyApp(cmd2.Cmd):
9091
def do_foo(self, args):
9192
"""This docstring is the built-in help for the foo command."""
92-
print('foo bar baz')
93+
print(cmd2.ansi.style('foo bar baz', fg='red'))
9394
```
9495
- By default the docstring for your **do_foo** method is the help for the **foo** command
9596
- NOTE: This doesn't apply if you use one of the `argparse` decorators mentioned below
@@ -314,11 +315,10 @@ example/transcript_regex.txt:
314315

315316
```text
316317
# Run this transcript with "python example.py -t transcript_regex.txt"
317-
# The regex for colors is because no color on Windows.
318318
# The regex for editor will match whatever program you use.
319319
# regexes on prompts just make the trailing space obvious
320320
(Cmd) set
321-
colors: /(True|False)/
321+
allow_ansi: Terminal
322322
continuation_prompt: >/ /
323323
debug: False
324324
echo: False
@@ -331,9 +331,7 @@ quiet: False
331331
timing: False
332332
```
333333

334-
Note how a regular expression `/(True|False)/` is used for output of the **show color** command since
335-
colored text is currently not available for cmd2 on Windows. Regular expressions can be used anywhere within a
336-
transcript file simply by enclosing them within forward slashes, `/`.
334+
Regular expressions can be used anywhere within a transcript file simply by enclosing them within forward slashes, `/`.
337335

338336

339337
Found a bug?
@@ -357,12 +355,16 @@ Here are a few examples of open-source projects which use `cmd2`:
357355
* [Ceph](https://ceph.com/) is a distributed object, block, and file storage platform
358356
* [JSShell](https://github.com/Den1al/JSShell)
359357
* An interactive multi-user web JavaScript shell
358+
* [psiTurk](https://psiturk.org)
359+
* An open platform for science on Amazon Mechanical Turk
360360
* [Jok3r](http://www.jok3r-framework.com)
361361
* Network & Web Pentest Automation Framework
362362
* [Poseidon](https://github.com/CyberReboot/poseidon)
363363
* Leverages software-defined networks (SDNs) to acquire and then feed network traffic to a number of machine learning techniques
364364
* [Unipacker](https://github.com/unipacker/unipacker)
365365
* Automatic and platform-independent unpacker for Windows binaries based on emulation
366+
* [FLASHMINGO](https://github.com/fireeye/flashmingo)
367+
* Automatic analysis of SWF files based on some heuristics. Extensible via plugins.
366368
* [tomcatmanager](https://github.com/tomcatmanager/tomcatmanager)
367369
* A command line tool and python library for managing a tomcat server
368370
* [mptcpanalyzer](https://github.com/teto/mptcpanalyzer)

cmd2/ansi.py

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
# coding=utf-8
2+
"""Support for ANSI escape sequences which are used for things like applying style to text"""
3+
import functools
4+
import re
5+
from typing import Any, IO
6+
7+
import colorama
8+
from colorama import Fore, Back, Style
9+
from wcwidth import wcswidth
10+
11+
# Values for allow_ansi setting
12+
ANSI_NEVER = 'Never'
13+
ANSI_TERMINAL = 'Terminal'
14+
ANSI_ALWAYS = 'Always'
15+
16+
# Controls when ANSI escape sequences are allowed in output
17+
allow_ansi = ANSI_TERMINAL
18+
19+
# Regular expression to match ANSI escape sequences
20+
ANSI_ESCAPE_RE = re.compile(r'\x1b[^m]*m')
21+
22+
# Foreground color presets
23+
FG_COLORS = {
24+
'black': Fore.BLACK,
25+
'red': Fore.RED,
26+
'green': Fore.GREEN,
27+
'yellow': Fore.YELLOW,
28+
'blue': Fore.BLUE,
29+
'magenta': Fore.MAGENTA,
30+
'cyan': Fore.CYAN,
31+
'white': Fore.WHITE,
32+
'bright_black': Fore.LIGHTBLACK_EX,
33+
'bright_red': Fore.LIGHTRED_EX,
34+
'bright_green': Fore.LIGHTGREEN_EX,
35+
'bright_yellow': Fore.LIGHTYELLOW_EX,
36+
'bright_blue': Fore.LIGHTBLUE_EX,
37+
'bright_magenta': Fore.LIGHTMAGENTA_EX,
38+
'bright_cyan': Fore.LIGHTCYAN_EX,
39+
'bright_white': Fore.LIGHTWHITE_EX,
40+
'reset': Fore.RESET,
41+
}
42+
43+
# Background color presets
44+
BG_COLORS = {
45+
'black': Back.BLACK,
46+
'red': Back.RED,
47+
'green': Back.GREEN,
48+
'yellow': Back.YELLOW,
49+
'blue': Back.BLUE,
50+
'magenta': Back.MAGENTA,
51+
'cyan': Back.CYAN,
52+
'white': Back.WHITE,
53+
'bright_black': Back.LIGHTBLACK_EX,
54+
'bright_red': Back.LIGHTRED_EX,
55+
'bright_green': Back.LIGHTGREEN_EX,
56+
'bright_yellow': Back.LIGHTYELLOW_EX,
57+
'bright_blue': Back.LIGHTBLUE_EX,
58+
'bright_magenta': Back.LIGHTMAGENTA_EX,
59+
'bright_cyan': Back.LIGHTCYAN_EX,
60+
'bright_white': Back.LIGHTWHITE_EX,
61+
'reset': Back.RESET,
62+
}
63+
64+
FG_RESET = FG_COLORS['reset']
65+
BG_RESET = BG_COLORS['reset']
66+
RESET_ALL = Style.RESET_ALL
67+
68+
# ANSI escape sequences not provided by colorama
69+
UNDERLINE_ENABLE = colorama.ansi.code_to_chars(4)
70+
UNDERLINE_DISABLE = colorama.ansi.code_to_chars(24)
71+
72+
73+
def strip_ansi(text: str) -> str:
74+
"""
75+
Strip ANSI escape sequences from a string.
76+
77+
:param text: string which may contain ANSI escape sequences
78+
:return: the same string with any ANSI escape sequences removed
79+
"""
80+
return ANSI_ESCAPE_RE.sub('', text)
81+
82+
83+
def ansi_safe_wcswidth(text: str) -> int:
84+
"""
85+
Wrap wcswidth to make it compatible with strings that contains ANSI escape sequences
86+
87+
:param text: the string being measured
88+
"""
89+
# Strip ANSI escape sequences since they cause wcswidth to return -1
90+
return wcswidth(strip_ansi(text))
91+
92+
93+
def ansi_aware_write(fileobj: IO, msg: str) -> None:
94+
"""
95+
Write a string to a fileobject and strip its ANSI escape sequences if required by allow_ansi setting
96+
97+
:param fileobj: the file object being written to
98+
:param msg: the string being written
99+
"""
100+
if allow_ansi.lower() == ANSI_NEVER.lower() or \
101+
(allow_ansi.lower() == ANSI_TERMINAL.lower() and not fileobj.isatty()):
102+
msg = strip_ansi(msg)
103+
fileobj.write(msg)
104+
105+
106+
def fg_lookup(fg_name: str) -> str:
107+
"""Look up ANSI escape codes based on foreground color name.
108+
109+
:param fg_name: foreground color name to look up ANSI escape code(s) for
110+
:return: ANSI escape code(s) associated with this color
111+
:raises ValueError if the color cannot be found
112+
"""
113+
try:
114+
ansi_escape = FG_COLORS[fg_name.lower()]
115+
except KeyError:
116+
raise ValueError('Foreground color {!r} does not exist.'.format(fg_name))
117+
return ansi_escape
118+
119+
120+
def bg_lookup(bg_name: str) -> str:
121+
"""Look up ANSI escape codes based on background color name.
122+
123+
:param bg_name: background color name to look up ANSI escape code(s) for
124+
:return: ANSI escape code(s) associated with this color
125+
:raises ValueError if the color cannot be found
126+
"""
127+
try:
128+
ansi_escape = BG_COLORS[bg_name.lower()]
129+
except KeyError:
130+
raise ValueError('Background color {!r} does not exist.'.format(bg_name))
131+
return ansi_escape
132+
133+
134+
def style(text: Any, *, fg: str = '', bg: str = '', bold: bool = False, underline: bool = False) -> str:
135+
"""Styles a string with ANSI colors and/or styles and returns the new string.
136+
137+
The styling is self contained which means that at the end of the string reset code(s) are issued
138+
to undo whatever styling was done at the beginning.
139+
140+
:param text: Any object compatible with str.format()
141+
:param fg: foreground color. Relies on `fg_lookup()` to retrieve ANSI escape based on name. Defaults to no color.
142+
:param bg: background color. Relies on `bg_lookup()` to retrieve ANSI escape based on name. Defaults to no color.
143+
:param bold: apply the bold style if True. Defaults to False.
144+
:param underline: apply the underline style if True. Defaults to False.
145+
:return: the stylized string
146+
"""
147+
# List of strings that add style
148+
additions = []
149+
150+
# List of strings that remove style
151+
removals = []
152+
153+
# Convert the text object into a string if it isn't already one
154+
text = "{}".format(text)
155+
156+
# Process the style settings
157+
if fg:
158+
additions.append(fg_lookup(fg))
159+
removals.append(FG_RESET)
160+
161+
if bg:
162+
additions.append(bg_lookup(bg))
163+
removals.append(BG_RESET)
164+
165+
if bold:
166+
additions.append(Style.BRIGHT)
167+
removals.append(Style.NORMAL)
168+
169+
if underline:
170+
additions.append(UNDERLINE_ENABLE)
171+
removals.append(UNDERLINE_DISABLE)
172+
173+
# Combine the ANSI escape sequences with the text
174+
return "".join(additions) + text + "".join(removals)
175+
176+
177+
# Default styles for printing strings of various types.
178+
# These can be altered to suit an application's needs and only need to be a
179+
# function with the following structure: func(str) -> str
180+
style_success = functools.partial(style, fg='green', bold=True)
181+
style_warning = functools.partial(style, fg='bright_yellow')
182+
style_error = functools.partial(style, fg='bright_red')

cmd2/argparse_completer.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,8 @@ def my_completer(text: str, line: str, begidx: int, endidx:int, extra_param: str
6666
from argparse import ZERO_OR_MORE, ONE_OR_MORE, ArgumentError, _, _get_action_name, SUPPRESS
6767
from typing import List, Dict, Tuple, Callable, Union
6868

69-
from colorama import Fore
70-
69+
from .ansi import ansi_aware_write, ansi_safe_wcswidth, style_error
7170
from .rl_utils import rl_force_redisplay
72-
from .utils import ansi_safe_wcswidth
7371

7472
# attribute that can optionally added to an argparse argument (called an Action) to
7573
# define the completion choices for the argument. You may provide a Collection or a Function.
@@ -996,7 +994,8 @@ def error(self, message: str) -> None:
996994
linum += 1
997995

998996
self.print_usage(sys.stderr)
999-
self.exit(2, Fore.LIGHTRED_EX + '{}\n\n'.format(formatted_message) + Fore.RESET)
997+
formatted_message = style_error(formatted_message)
998+
self.exit(2, '{}\n\n'.format(formatted_message))
1000999

10011000
def format_help(self) -> str:
10021001
"""Copy of format_help() from argparse.ArgumentParser with tweaks to separately display required parameters"""
@@ -1048,6 +1047,13 @@ def format_help(self) -> str:
10481047
# determine help from format above
10491048
return formatter.format_help() + '\n'
10501049

1050+
def _print_message(self, message, file=None):
1051+
# Override _print_message to use ansi_aware_write() since we use ANSI escape characters to support color
1052+
if message:
1053+
if file is None:
1054+
file = _sys.stderr
1055+
ansi_aware_write(file, message)
1056+
10511057
def _get_nargs_pattern(self, action) -> str:
10521058
# Override _get_nargs_pattern behavior to use the nargs ranges provided by AutoCompleter
10531059
if isinstance(action, _RangeAction) and \

0 commit comments

Comments
 (0)