3232import argparse
3333import cmd
3434import collections
35+ import colorama
3536from colorama import Fore
3637import glob
3738import inspect
3839import os
39- import platform
4040import re
4141import shlex
4242import sys
4343import threading
44- from typing import Any , Callable , Dict , List , Mapping , Optional , Tuple , Type , Union
44+ from typing import Any , Callable , Dict , List , Mapping , Optional , Tuple , Type , Union , IO
4545
4646from . import constants
4747from . import utils
@@ -318,7 +318,7 @@ class Cmd(cmd.Cmd):
318318 reserved_words = []
319319
320320 # Attributes which ARE dynamically settable at runtime
321- colors = ( platform . system () != 'Windows' )
321+ colors = constants . COLORS_TERMINAL
322322 continuation_prompt = '> '
323323 debug = False
324324 echo = False
@@ -338,7 +338,7 @@ class Cmd(cmd.Cmd):
338338
339339 # To make an attribute settable with the "do_set" command, add it to this ...
340340 # This starts out as a dictionary but gets converted to an OrderedDict sorted alphabetically by key
341- settable = {'colors' : 'Colorized output (*nix only )' ,
341+ settable = {'colors' : 'Allow colorized output (valid values: Terminal, Always, Never )' ,
342342 'continuation_prompt' : 'On 2nd+ line of input' ,
343343 'debug' : 'Show full error stack on error' ,
344344 'echo' : 'Echo command issued into output' ,
@@ -370,6 +370,9 @@ def __init__(self, completekey: str='tab', stdin=None, stdout=None, persistent_h
370370 except AttributeError :
371371 pass
372372
373+ # Override whether ansi codes should be stripped from the output since cmd2 has its own logic for doing this
374+ colorama .init (strip = False )
375+
373376 # initialize plugin system
374377 # needs to be done before we call __init__(0)
375378 self ._initialize_plugin_system ()
@@ -418,13 +421,13 @@ def __init__(self, completekey: str='tab', stdin=None, stdout=None, persistent_h
418421 self ._STOP_AND_EXIT = True # cmd convention
419422
420423 self ._colorcodes = {'bold' : {True : '\x1b [1m' , False : '\x1b [22m' },
421- 'cyan' : {True : ' \x1b [36m' , False : ' \x1b [39m' },
422- 'blue' : {True : ' \x1b [34m' , False : ' \x1b [39m' },
423- 'red' : {True : ' \x1b [31m' , False : ' \x1b [39m' },
424- 'magenta' : {True : ' \x1b [35m' , False : ' \x1b [39m' },
425- 'green' : {True : ' \x1b [32m' , False : ' \x1b [39m' },
426- 'underline' : {True : '\x1b [4m' , False : ' \x1b [24m' },
427- 'yellow' : {True : ' \x1b [33m' , False : ' \x1b [39m' }}
424+ 'cyan' : {True : Fore . CYAN , False : Fore . RESET },
425+ 'blue' : {True : Fore . BLUE , False : Fore . RESET },
426+ 'red' : {True : Fore . RED , False : Fore . RESET },
427+ 'magenta' : {True : Fore . MAGENTA , False : Fore . RESET },
428+ 'green' : {True : Fore . GREEN , False : Fore . RESET },
429+ 'underline' : {True : '\x1b [4m' , False : Fore . RESET },
430+ 'yellow' : {True : Fore . YELLOW , False : Fore . RESET }}
428431
429432 # Used load command to store the current script dir as a LIFO queue to support _relative_load command
430433 self ._script_dir = []
@@ -554,49 +557,69 @@ def _finalize_app_parameters(self) -> None:
554557 # Make sure settable parameters are sorted alphabetically by key
555558 self .settable = collections .OrderedDict (sorted (self .settable .items (), key = lambda t : t [0 ]))
556559
557- def poutput (self , msg : str , end : str = '\n ' ) -> None :
558- """Convenient shortcut for self.stdout.write(); by default adds newline to end if not already present.
560+ def decolorized_write (self , fileobj : IO , msg : str ) -> None :
561+ """Write a string to a fileobject, stripping ANSI escape sequences if necessary
562+
563+ Honor the current colors setting, which requires us to check whether the
564+ fileobject is a tty.
565+ """
566+ if self .colors .lower () == constants .COLORS_NEVER .lower () or \
567+ (self .colors .lower () == constants .COLORS_TERMINAL .lower () and not fileobj .isatty ()):
568+ msg = utils .strip_ansi (msg )
569+ fileobj .write (msg )
559570
560- Also handles BrokenPipeError exceptions for when a commands's output has been piped to another process and
561- that process terminates before the cmd2 command is finished executing.
571+ def poutput (self , msg : Any , end : str = '\n ' , color : str = '' ) -> None :
572+ """Smarter self.stdout.write(); color aware and adds newline of not present.
573+
574+ Also handles BrokenPipeError exceptions for when a commands's output has
575+ been piped to another process and that process terminates before the
576+ cmd2 command is finished executing.
562577
563578 :param msg: message to print to current stdout (anything convertible to a str with '{}'.format() is OK)
564- :param end: string appended after the end of the message if not already present, default a newline
579+ :param end: (optional) string appended after the end of the message if not already present, default a newline
580+ :param color: (optional) color escape to output this message with
565581 """
566582 if msg is not None and msg != '' :
567583 try :
568584 msg_str = '{}' .format (msg )
569- self .stdout .write (msg_str )
570585 if not msg_str .endswith (end ):
571- self .stdout .write (end )
586+ msg_str += end
587+ if color :
588+ msg_str = color + msg_str + Fore .RESET
589+ self .decolorized_write (self .stdout , msg_str )
572590 except BrokenPipeError :
573- # This occurs if a command's output is being piped to another process and that process closes before the
574- # command is finished. If you would like your application to print a warning message, then set the
575- # broken_pipe_warning attribute to the message you want printed.
591+ # This occurs if a command's output is being piped to another
592+ # process and that process closes before the command is
593+ # finished. If you would like your application to print a
594+ # warning message, then set the broken_pipe_warning attribute
595+ # to the message you want printed.
576596 if self .broken_pipe_warning :
577597 sys .stderr .write (self .broken_pipe_warning )
578598
579- def perror (self , err : Union [str , Exception ], traceback_war : bool = True ) -> None :
599+ def perror (self , err : Union [str , Exception ], traceback_war : bool = True , err_color : str = Fore .LIGHTRED_EX ,
600+ war_color : str = Fore .LIGHTYELLOW_EX ) -> None :
580601 """ Print error message to sys.stderr and if debug is true, print an exception Traceback if one exists.
581602
582603 :param err: an Exception or error message to print out
583604 :param traceback_war: (optional) if True, print a message to let user know they can enable debug
584- :return:
605+ :param err_color: (optional) color escape to output error with
606+ :param war_color: (optional) color escape to output warning with
585607 """
586608 if self .debug :
587609 import traceback
588610 traceback .print_exc ()
589611
590612 if isinstance (err , Exception ):
591613 err_msg = "EXCEPTION of type '{}' occurred with message: '{}'\n " .format (type (err ).__name__ , err )
592- sys .stderr .write (self .colorize (err_msg , 'red' ))
593614 else :
594- err_msg = self .colorize ("ERROR: {}\n " .format (err ), 'red' )
595- sys .stderr .write (err_msg )
615+ err_msg = "ERROR: {}\n " .format (err )
616+ err_msg = err_color + err_msg + Fore .RESET
617+ self .decolorized_write (sys .stderr , err_msg )
596618
597619 if traceback_war :
598620 war = "To enable full traceback, run the following command: 'set debug true'\n "
599- sys .stderr .write (self .colorize (war , 'yellow' ))
621+ war = war_color + war + Fore .RESET
622+ self .decolorized_write (sys .stderr , war )
600623
601624 def pfeedback (self , msg : str ) -> None :
602625 """For printing nonessential feedback. Can be silenced with `quiet`.
@@ -605,7 +628,7 @@ def pfeedback(self, msg: str) -> None:
605628 if self .feedback_to_output :
606629 self .poutput (msg )
607630 else :
608- sys .stderr . write ( "{}\n " .format (msg ))
631+ self . decolorized_write ( sys .stderr , "{}\n " .format (msg ))
609632
610633 def ppaged (self , msg : str , end : str = '\n ' , chop : bool = False ) -> None :
611634 """Print output using a pager if it would go off screen and stdout isn't currently being redirected.
@@ -641,6 +664,9 @@ def ppaged(self, msg: str, end: str='\n', chop: bool=False) -> None:
641664 # Don't attempt to use a pager that can block if redirecting or running a script (either text or Python)
642665 # Also only attempt to use a pager if actually running in a real fully functional terminal
643666 if functional_terminal and not self .redirecting and not self ._in_py and not self ._script_dir :
667+ if self .colors .lower () == constants .COLORS_NEVER .lower ():
668+ msg_str = utils .strip_ansi (msg_str )
669+
644670 pager = self .pager
645671 if chop :
646672 pager = self .pager_chop
@@ -665,7 +691,7 @@ def ppaged(self, msg: str, end: str='\n', chop: bool=False) -> None:
665691 except BrokenPipeError :
666692 # This occurs if a command's output is being piped to another process and that process closes before the
667693 # command is finished. If you would like your application to print a warning message, then set the
668- # broken_pipe_warning attribute to the message you want printed.
694+ # broken_pipe_warning attribute to the message you want printed.`
669695 if self .broken_pipe_warning :
670696 sys .stderr .write (self .broken_pipe_warning )
671697
@@ -676,7 +702,7 @@ def colorize(self, val: str, color: str) -> str:
676702 is running on Windows, will return ``val`` unchanged.
677703 ``color`` should be one of the supported strings (or styles):
678704 red/blue/green/cyan/magenta, bold, underline"""
679- if self .colors and (self .stdout == self .initial_stdout ):
705+ if self .colors . lower () != constants . COLORS_NEVER . lower () and (self .stdout == self .initial_stdout ):
680706 return self ._colorcodes [color ][True ] + val + self ._colorcodes [color ][False ]
681707 return val
682708
0 commit comments