diff --git a/docs/usage.rst b/docs/usage.rst index 2f18087b..879d4ee6 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -203,6 +203,16 @@ will stack. For example, the following are equivalent to setting the verbosity l $ ansible-builder build -v -v -v +``--no-colors`` +*************** + +Disables ANSI text colors. + +If this option is not given, the default will be to enable text colors for the output. +The ``NO_COLOR`` and ``FORCE_COLOR`` environment variables will be honored if this CLI option +is not supplied. + + ``--prune-images`` ****************** diff --git a/src/ansible_builder/cli.py b/src/ansible_builder/cli.py index b3a6a784..a1d347b1 100644 --- a/src/ansible_builder/cli.py +++ b/src/ansible_builder/cli.py @@ -6,7 +6,6 @@ from . import constants -from .colors import MessageColors from .exceptions import DefinitionError from .main import AnsibleBuilder from .policies import PolicyChoices @@ -40,19 +39,55 @@ def __call__(self, parser, namespace, values, option_string=None): setattr(namespace, self.dest, self.count) +def _should_disable_colors() -> bool: + """ + Check the environment to decide if text colorization should be disabled. + + According to no-color.org, if NO_COLOR is present, and not an empty string (regardless of + its value), text should not be colorized. + + According to force-color.org, if FORCE_COLOR is present, and not an empty string (regardless + of its value), text should be colorized, and should trump NO_COLOR. + + :returns: True if colors are disabled, False if enabled. + """ + disabled = False + + if os.environ.get('TERM', '') == 'dumb': + return True + + no_color = os.environ.get('NO_COLOR', None) + force_color = os.environ.get('FORCE_COLOR', None) + + if no_color: + disabled = True + if force_color: + disabled = False + + return disabled + + def run(): args = parse_args() - configure_logger(args.verbosity) + + # If user explicitly requests to disable colors, that value takes precedence. Otherwise, + # we'll check the environment. + disable_colors = args.no_colors + if '--no-colors' not in sys.argv: + disable_colors = _should_disable_colors() + + configure_logger(args.verbosity, disable_colors) if args.action in ['create', 'build']: - ab = AnsibleBuilder(**vars(args)) + kwargs = vars(args) + kwargs.pop('no_colors') # not a value we should pass along + + ab = AnsibleBuilder(**kwargs) action = getattr(ab, ab.action) try: if action(): - print( - f"{MessageColors.OKGREEN}Complete! The build context can be found at: " - f"{os.path.abspath(ab.build_context)}{MessageColors.ENDC}" - ) + logger.log(constants.SUCCESS_LOGLEVEL, + "Complete! The build context can be found at: %s", os.path.abspath(ab.build_context)) sys.exit(0) except DefinitionError as e: logger.error(e.args[0]) @@ -200,6 +235,12 @@ def add_container_options(parser): 'Integer values are also accepted (for example, "-v3" or "--verbosity 3"). ' 'Default is %(default)s.') + n.add_argument('--no-colors', + dest='no_colors', + action='store_true', + help='Disable ANSI text colors (enabled by default). NO_COLOR and FORCE_COLOR environment ' + 'variables will be honored if this option is not used.') + def parse_args(args=None): diff --git a/src/ansible_builder/colors.py b/src/ansible_builder/colors.py deleted file mode 100644 index 5f868d0a..00000000 --- a/src/ansible_builder/colors.py +++ /dev/null @@ -1,7 +0,0 @@ -class MessageColors: - HEADER = '\033[94m' - OKGREEN = '\033[92m' - WARNING = '\033[93m' - FAIL = '\033[1m' - OK = '\033[95m' - ENDC = '\033[0m' diff --git a/src/ansible_builder/constants.py b/src/ansible_builder/constants.py index 87088b32..d1d555a0 100644 --- a/src/ansible_builder/constants.py +++ b/src/ansible_builder/constants.py @@ -48,3 +48,5 @@ DEFAULT_EE_BASENAME = "execution-environment" YAML_FILENAME_EXTENSIONS = ('yml', 'yaml') + +SUCCESS_LOGLEVEL = 100 diff --git a/src/ansible_builder/utils.py b/src/ansible_builder/utils.py index 8b74567c..ff62627e 100644 --- a/src/ansible_builder/utils.py +++ b/src/ansible_builder/utils.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import filecmp import logging import logging.config @@ -9,58 +11,50 @@ from collections import deque from pathlib import Path -from .colors import MessageColors from . import constants logger = logging.getLogger(__name__) -logging_levels = { - '0': 'ERROR', - '1': 'WARNING', - '2': 'INFO', - '3': 'DEBUG', -} class ColorFilter(logging.Filter): + class MessageColors: + ERROR = '\033[91m' # bright red + WARNING = '\033[93m' # bright yellow + INFO = '\033[94m' # bright blue + DEBUG = '\033[95m' # bright magenta + OKGREEN = '\033[92m' # bright green + DEFAULT = '\033[0m' # terminal default + color_map = { - 'ERROR': MessageColors.FAIL, - 'WARNING': MessageColors.WARNING, - 'INFO': MessageColors.HEADER, - 'DEBUG': MessageColors.OK + logging.CRITICAL: MessageColors.ERROR, + logging.ERROR: MessageColors.ERROR, + logging.WARNING: MessageColors.WARNING, + logging.INFO: MessageColors.INFO, + logging.DEBUG: MessageColors.DEBUG, + constants.SUCCESS_LOGLEVEL: MessageColors.OKGREEN, } def filter(self, record): if sys.stdout.isatty(): - record.msg = self.color_map[record.levelname] + record.msg + MessageColors.ENDC + record.msg = self.color_map[record.levelno] + record.msg + ColorFilter.MessageColors.DEFAULT return record -LOGGING = { - 'version': 1, - 'filters': { - 'colorize': { - '()': ColorFilter - } - }, - 'handlers': { - 'console': { - 'class': 'logging.StreamHandler', - 'filters': ['colorize'], - 'stream': 'ext://sys.stdout' - } - }, - 'loggers': { - 'ansible_builder': { - 'handlers': ['console'], - } +def configure_logger(verbosity: int, disable_colors: bool = False): + logging_levels = { + 0: 'ERROR', + 1: 'WARNING', + 2: 'INFO', + 3: 'DEBUG', } -} - -def configure_logger(verbosity): - LOGGING['loggers']['ansible_builder']['level'] = logging_levels[str(verbosity)] - logging.config.dictConfig(LOGGING) + root_logger = logging.getLogger() + root_logger.setLevel(logging_levels[verbosity]) + handler = logging.StreamHandler(sys.stdout) + if not disable_colors: + handler.addFilter(ColorFilter()) + root_logger.addHandler(handler) def run_command(command, capture_output=False, allow_error=False): @@ -118,7 +112,7 @@ def run_command(command, capture_output=False, allow_error=False): logger.error("An error occurred (rc=%s), see output line(s) above for details.", rc) sys.exit(1) - return (rc, output) + return rc, output def write_file(filename: str, lines: list) -> bool: @@ -170,9 +164,9 @@ def copy_file(source: str, dest: str, ignore_mtime: bool = False) -> bool: to copy the file if it doesn't exist, or if it has changed between builds. See the `copy_directory()` function for the directory copy equivalent. - :param source str: Path to a source file. - :param dest str: Path to a destination file within the context subdir. - :param ignore_mtime bool: Whether or not mtime should be considered. + :param str source: Path to a source file. + :param str dest: Path to a destination file within the context subdir. + :param bool ignore_mtime: Whether mtime should be considered. :returns: True if the file was copied, False if not. diff --git a/test/unit/test_cli.py b/test/unit/test_cli.py index 4ac285ec..bf5e98ba 100644 --- a/test/unit/test_cli.py +++ b/test/unit/test_cli.py @@ -12,7 +12,9 @@ def prepare(args): args = parse_args(args) - return AnsibleBuilder(**vars(args)) + kwargs = vars(args) + kwargs.pop('no_colors') + return AnsibleBuilder(**kwargs) def test_custom_image(exec_env_definition_file, tmp_path):