Add --color=[always|never|auto] argument; fix color when piping (#3013)

* Disable spec colorization when redirecting stdout and add command line flag to re-enable
* Add command line `--color` flag to control output colorization
* Add options to `llnl.util.tty.color` to allow color to be auto/always/never
* Add `Spec.cformat()` function to be used when `format()` should have auto-coloring
This commit is contained in:
paulhopkins 2017-07-31 20:57:47 +01:00 committed by Todd Gamblin
parent f3c70c235c
commit 1c7e5724d9
14 changed files with 105 additions and 45 deletions

View File

@ -80,6 +80,7 @@
"""
import re
import sys
from contextlib import contextmanager
class ColorParseError(Exception):
@ -107,15 +108,62 @@ def __init__(self, message):
# Regex to be used for color formatting
color_re = r'@(?:@|\.|([*_])?([a-zA-Z])?(?:{((?:[^}]|}})*)})?)'
# Mapping from color arguments to values for tty.set_color
color_when_values = {
'always': True,
'auto': None,
'never': False
}
# Force color even if stdout is not a tty.
_force_color = False
# Force color; None: Only color if stdout is a tty
# True: Always colorize output, False: Never colorize output
_force_color = None
def _color_when_value(when):
"""Raise a ValueError for an invalid color setting.
Valid values are 'always', 'never', and 'auto', or equivalently,
True, False, and None.
"""
if when in color_when_values:
return color_when_values[when]
elif when not in color_when_values.values():
raise ValueError('Invalid color setting: %s' % when)
return when
def get_color_when():
"""Return whether commands should print color or not."""
if _force_color is not None:
return _force_color
return sys.stdout.isatty()
def set_color_when(when):
"""Set when color should be applied. Options are:
* True or 'always': always print color
* False or 'never': never print color
* None or 'auto': only print color if sys.stdout is a tty.
"""
global _force_color
_force_color = _color_when_value(when)
@contextmanager
def color_when(value):
"""Context manager to temporarily use a particular color setting."""
old_value = value
set_color(value)
yield
set_color(old_value)
class match_to_ansi(object):
def __init__(self, color=True):
self.color = color
self.color = _color_when_value(color)
def escape(self, s):
"""Returns a TTY escape sequence for a color"""
@ -166,7 +214,7 @@ def colorize(string, **kwargs):
color (bool): If False, output will be plain text without control
codes, for output to non-console devices.
"""
color = kwargs.get('color', True)
color = _color_when_value(kwargs.get('color', get_color_when()))
return re.sub(color_re, match_to_ansi(color), string)
@ -188,7 +236,7 @@ def cwrite(string, stream=sys.stdout, color=None):
then it will be set based on stream.isatty().
"""
if color is None:
color = stream.isatty() or _force_color
color = get_color_when()
stream.write(colorize(string, color=color))
@ -217,7 +265,7 @@ def write(self, string, **kwargs):
if raw:
color = True
else:
color = self._stream.isatty() or _force_color
color = get_color_when()
raw_write(colorize(string, color=color))
def writelines(self, sequence, **kwargs):

View File

@ -154,9 +154,8 @@ def disambiguate_spec(spec):
elif len(matching_specs) > 1:
args = ["%s matches multiple packages." % spec,
"Matching packages:"]
color = sys.stdout.isatty()
args += [colorize(" @K{%s} " % s.dag_hash(7), color=color) +
s.format('$_$@$%@$=', color=color) for s in matching_specs]
args += [colorize(" @K{%s} " % s.dag_hash(7)) +
s.cformat('$_$@$%@$=') for s in matching_specs]
args += ["Use a more specific spec."]
tty.die(*args)
@ -200,7 +199,7 @@ def display_specs(specs, **kwargs):
specs = index[(architecture, compiler)]
specs.sort()
abbreviated = [s.format(format_string, color=True) for s in specs]
abbreviated = [s.cformat(format_string) for s in specs]
if mode == 'paths':
# Print one spec per line along with prefix path
width = max(len(s) for s in abbreviated)
@ -215,7 +214,6 @@ def display_specs(specs, **kwargs):
for spec in specs:
print(spec.tree(
format=format_string,
color=True,
indent=4,
prefix=(lambda s: gray_hash(s, hlen)) if hashes else None))
@ -227,7 +225,7 @@ def fmt(s):
string = ""
if hashes:
string += gray_hash(s, hlen) + ' '
string += s.format('$-%s$@%s' % (nfmt, vfmt), color=True)
string += s.cformat('$-%s$@%s' % (nfmt, vfmt))
return string
@ -237,7 +235,7 @@ def fmt(s):
for spec in specs:
# Print the hash if necessary
hsh = gray_hash(spec, hlen) + ' ' if hashes else ''
print(hsh + spec.format(format_string, color=True) + '\n')
print(hsh + spec.cformat(format_string) + '\n')
else:
raise ValueError(

View File

@ -47,7 +47,7 @@ def dependents(parser, args):
tty.die("spack dependents takes only one spec.")
spec = spack.cmd.disambiguate_spec(specs[0])
tty.msg("Dependents of %s" % spec.format('$_$@$%@$/', color=True))
tty.msg("Dependents of %s" % spec.cformat('$_$@$%@$/'))
deps = spack.store.db.installed_dependents(spec)
if deps:
spack.cmd.display_specs(deps)

View File

@ -213,7 +213,7 @@ def mirror_create(args):
" %-4d failed to fetch." % e)
if error:
tty.error("Failed downloads:")
colify(s.format("$_$@") for s in error)
colify(s.cformat("$_$@") for s in error)
def mirror(parser, args):

View File

@ -236,7 +236,8 @@ def refresh(mtype, specs, args):
if len(writer_list) > 1:
message += '\nfile: {0}\n'.format(filename)
for x in writer_list:
message += 'spec: {0}\n'.format(x.spec.format(color=True))
message += 'spec: {0}\n'.format(x.spec.format())
tty.error(message)
tty.error('Operation aborted')
raise SystemExit(1)
@ -269,7 +270,7 @@ def module(parser, args):
"and this is not allowed in this context")
tty.error(message.format(query=constraint))
for s in specs:
sys.stderr.write(s.format(color=True) + '\n')
sys.stderr.write(s.cformat() + '\n')
raise SystemExit(1)
except NoMatch:
message = ("the constraint '{query}' matches no package, "

View File

@ -60,8 +60,7 @@ def setup_parser(subparser):
def spec(parser, args):
name_fmt = '$.' if args.namespaces else '$_'
kwargs = {'color': True,
'cover': args.cover,
kwargs = {'cover': args.cover,
'format': name_fmt + '$@$%@+$+$=',
'hashes': args.long or args.very_long,
'hashlen': None if args.very_long else 7,

View File

@ -180,8 +180,7 @@ def get_uninstall_list(args):
has_error = False
if dependent_list and not args.dependents and not args.force:
for spec, lst in dependent_list.items():
tty.error('Will not uninstall {0}'.format(
spec.format("$_$@$%@$/", color=True)))
tty.error("Will not uninstall %s" % spec.cformat("$_$@$%@$/"))
print('')
print('The following packages depend on it:')
spack.cmd.display_specs(lst, **display_args)

View File

@ -288,7 +288,7 @@ def _assign_dependencies(self, hash_key, installs, data):
if dhash not in data:
tty.warn("Missing dependency not in database: ",
"%s needs %s-%s" % (
spec.format('$_$/'), dname, dhash[:7]))
spec.cformat('$_$/'), dname, dhash[:7]))
continue
child = data[dhash].spec
@ -440,8 +440,7 @@ def _read_suppress_error():
# just to be conservative in case a command like
# "autoremove" is run by the user after a reindex.
tty.debug(
'RECONSTRUCTING FROM SPEC.YAML: {0}'.format(spec)
)
'RECONSTRUCTING FROM SPEC.YAML: {0}'.format(spec))
explicit = True
if old_data is not None:
old_info = old_data.get(spec.dag_hash())
@ -467,8 +466,7 @@ def _read_suppress_error():
# installed compilers or externally installed
# applications.
tty.debug(
'RECONSTRUCTING FROM OLD DB: {0}'.format(entry.spec)
)
'RECONSTRUCTING FROM OLD DB: {0}'.format(entry.spec))
try:
layout = spack.store.layout
if entry.spec.external:

View File

@ -168,7 +168,9 @@ def __init__(self, root, **kwargs):
self.metadata_dir = kwargs.get('metadata_dir', '.spack')
self.hash_len = kwargs.get('hash_len')
self.path_scheme = kwargs.get('path_scheme') or (
"${ARCHITECTURE}/${COMPILERNAME}-${COMPILERVER}/${PACKAGE}-${VERSION}-${HASH}") # NOQA: E501
"${ARCHITECTURE}/"
"${COMPILERNAME}-${COMPILERVER}/"
"${PACKAGE}-${VERSION}-${HASH}")
if self.hash_len is not None:
if re.search('\${HASH:\d+}', self.path_scheme):
raise InvalidDirectoryLayoutParametersError(

View File

@ -58,7 +58,7 @@
# control top-level spack options shown in basic vs. advanced help
options_by_level = {
'short': 'hkV',
'short': ['h', 'k', 'V', 'color'],
'long': 'all'
}
@ -280,6 +280,9 @@ def make_argument_parser():
parser.add_argument('-h', '--help', action='store_true',
help="show this help message and exit")
parser.add_argument('--color', action='store', default='auto',
choices=('always', 'never', 'auto'),
help="when to colorize output; default is auto")
parser.add_argument('-d', '--debug', action='store_true',
help="write out debug logs during compile")
parser.add_argument('-D', '--pdb', action='store_true',
@ -325,6 +328,9 @@ def setup_main_options(args):
tty.warn("You asked for --insecure. Will NOT check SSL certificates.")
spack.insecure = True
# when to use color (takes always, auto, or never)
tty.color.set_color_when(args.color)
def allows_unknown_args(command):
"""Implements really simple argument injection for unknown arguments.

View File

@ -224,14 +224,14 @@ def add_single_spec(spec, mirror_root, categories, **kwargs):
# create a subdirectory for the current package@version
archive_path = os.path.abspath(join_path(
mirror_root, mirror_archive_path(spec, fetcher)))
name = spec.format("$_$@")
name = spec.cformat("$_$@")
else:
resource = stage.resource
archive_path = os.path.abspath(join_path(
mirror_root,
mirror_archive_path(spec, fetcher, resource.name)))
name = "{resource} ({pkg}).".format(
resource=resource.name, pkg=spec.format("$_$@"))
resource=resource.name, pkg=spec.cformat("$_$@"))
subdir = os.path.dirname(archive_path)
mkdirp(subdir)
@ -258,8 +258,8 @@ def add_single_spec(spec, mirror_root, categories, **kwargs):
if spack.debug:
sys.excepthook(*sys.exc_info())
else:
tty.warn("Error while fetching %s"
% spec.format('$_$@'), e.message)
tty.warn(
"Error while fetching %s" % spec.cformat('$_$@'), e.message)
categories['error'].append(spec)

View File

@ -905,7 +905,7 @@ def do_fetch(self, mirror_only=False):
start_time = time.time()
if spack.do_checksum and self.version not in self.versions:
tty.warn("There is no checksum on file to fetch %s safely." %
self.spec.format('$_$@'))
self.spec.cformat('$_$@'))
# Ask the user whether to skip the checksum if we're
# interactive, but just fail if non-interactive.
@ -1649,8 +1649,9 @@ def do_activate(self, force=False):
self.extendee_spec.package.activate(self, **self.extendee_args)
spack.store.layout.add_extension(self.extendee_spec, self.spec)
tty.msg("Activated extension %s for %s" %
(self.spec.short_spec, self.extendee_spec.format("$_$@$+$%@")))
tty.msg(
"Activated extension %s for %s" %
(self.spec.short_spec, self.extendee_spec.cformat("$_$@$+$%@")))
def dependency_activations(self):
return (spec for spec in self.spec.traverse(root=False, deptype='run')
@ -1708,8 +1709,9 @@ def do_deactivate(self, **kwargs):
spack.store.layout.remove_extension(
self.extendee_spec, self.spec)
tty.msg("Deactivated extension %s for %s" %
(self.spec.short_spec, self.extendee_spec.format("$_$@$+$%@")))
tty.msg(
"Deactivated extension %s for %s" %
(self.spec.short_spec, self.extendee_spec.cformat("$_$@$+$%@")))
def deactivate(self, extension, **kwargs):
"""Unlinks all files from extension out of this package's install dir.

View File

@ -108,6 +108,10 @@
from six import string_types
from six import iteritems
from llnl.util.filesystem import find_headers, find_libraries, is_exe
from llnl.util.lang import *
from llnl.util.tty.color import *
import spack
import spack.architecture
import spack.compilers as compilers
@ -117,9 +121,6 @@
import spack.util.spack_json as sjson
import spack.util.spack_yaml as syaml
from llnl.util.filesystem import find_headers, find_libraries, is_exe
from llnl.util.lang import *
from llnl.util.tty.color import *
from spack.util.module_cmd import get_path_from_module, load_module
from spack.error import SpecError, UnsatisfiableSpecError
from spack.provider_index import ProviderIndex
@ -1363,9 +1364,8 @@ def short_spec(self):
@property
def cshort_spec(self):
"""Returns a version of the spec with the dependencies hashed
instead of completely enumerated."""
return self.format('$_$@$%@$+$=$/', color=True)
"""Returns an auto-colorized version of ``self.short_spec``."""
return self.cformat('$_$@$%@$+$=$/')
@property
def prefix(self):
@ -2852,6 +2852,12 @@ def write(s, c):
result = out.getvalue()
return result
def cformat(self, *args, **kwargs):
"""Same as format, but color defaults to auto instead of False."""
kwargs = kwargs.copy()
kwargs.setdefault('color', None)
return self.format(*args, **kwargs)
def dep_string(self):
return ''.join("^" + dep.format() for dep in self.sorted_deps())
@ -2882,7 +2888,7 @@ def _installed_explicitly(self):
def tree(self, **kwargs):
"""Prints out this spec and its dependencies, tree-formatted
with indentation."""
color = kwargs.pop('color', False)
color = kwargs.pop('color', get_color_when())
depth = kwargs.pop('depth', False)
hashes = kwargs.pop('hashes', False)
hlen = kwargs.pop('hashlen', None)

View File

@ -115,7 +115,8 @@ function _spack {
if $list_options
then
compgen -W "-h --help -d --debug -D --pdb -k --insecure -m --mock -p
--profile -v --verbose -s --stacktrace -V --version" -- "$cur"
--profile -v --verbose -s --stacktrace -V --version
--color --color=always --color=auto --color=never" -- "$cur"
else
compgen -W "$(_subcommands)" -- "$cur"
fi