Repository: xonsh/xonsh

Issue:
Xonsh 0.8.11 fails on startup
```
⚡~$ pip3 install --force xonsh
Collecting xonsh
Installing collected packages: xonsh
  Found existing installation: xonsh 0.8.11
    Uninstalling xonsh-0.8.11:
      Successfully uninstalled xonsh-0.8.11
Successfully installed xonsh-0.8.11
⚡~$ xonsh --version
xonsh/0.8.11
⚡~$ xonsh
Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/xonsh/__amalgam__.py", line 24004, in main
    args = premain(argv)
  File "/usr/local/lib/python3.7/site-packages/xonsh/__amalgam__.py", line 23955, in premain
    env = start_services(shell_kwargs, args)
  File "/usr/local/lib/python3.7/site-packages/xonsh/__amalgam__.py", line 23890, in start_services
    cacheall=shell_kwargs.get("cacheall", False),
  File "/usr/local/lib/python3.7/site-packages/xonsh/__amalgam__.py", line 23021, in __init__
    load_builtins(execer=self, ctx=xonsh_ctx)
  File "/usr/local/lib/python3.7/site-packages/xonsh/__amalgam__.py", line 22592, in load_builtins
    builtins.__xonsh__.load(execer=execer, ctx=ctx)
  File "/usr/local/lib/python3.7/site-packages/xonsh/__amalgam__.py", line 22668, in load
    self.env = Env(default_env())
  File "/usr/local/lib/python3.7/site-packages/xonsh/__amalgam__.py", line 19110, in __init__
    self[key] = val
  File "/usr/local/lib/python3.7/site-packages/xonsh/__amalgam__.py", line 19272, in __setitem__
    val = ensurer.convert(val)
  File "/usr/local/lib/python3.7/site-packages/xonsh/__amalgam__.py", line 18218, in convert
    return cls.fromstring(x)
  File "/usr/local/lib/python3.7/site-packages/xonsh/__amalgam__.py", line 18181, in fromstring
    esc, "default", reversed_style=reversed_default
  File "/usr/local/lib/python3.7/site-packages/xonsh/__amalgam__.py", line 6986, in ansi_color_escape_code_to_name
    ec = ANSI_ESCAPE_CODE_RE.match(escape_code).group(2)
AttributeError: 'NoneType' object has no attribute 'group'
Xonsh encountered an issue during launch
Failback to /bin/bash
bash: shopt: globstar: invalid shell option name
bash: shopt: autocd: invalid shell option name
[
a few more failures because my bashrc depends on current
bash at /usr/local/bin/bash, not the old system bash
]
bash-3.2$ exit
```

I previously had 0.8.6 IIRC (which worked) and just installed 0.8.11. Was gonna use some low-energy time to configure my Xonsh config to get parity with Zsh to see if I could switch to Xonsh and then this happened :) Running on Mac Mojave. Lemme know if I can do anything else to help debug.

Discussion:

Patch:
diff --git a/docs/tutorial.rst b/docs/tutorial.rst
index a54cda10..38871e8b 100644
--- a/docs/tutorial.rst
+++ b/docs/tutorial.rst
@@ -1495,14 +1495,31 @@ or ``{BOLD_BLUE}``.  Colors have the form shown below:
   and ``BACKGROUND_#123456`` can both be used.
 * ``bg#HEX`` or ``BG#HEX`` are shortcuts for setting a background hex color.
   Thus you can set ``bg#0012ab`` or the uppercase version.
-* ``BOLD_`` is a prefix qualifier that may be used with any foreground color.
+* ``BOLD_`` is a prefix modifier that increases the intesnity of the font.
+  It may be used with any foreground color.
   For example, ``BOLD_RED`` and ``BOLD_#112233`` are OK!
+* ``FAINT_`` is a prefix modifier that decreases the intesnity of the font.
+  For example, ``FAINT_YELLOW``.
+* ``ITALIC_`` is a prefix modifier that switches to an italic font.
+  For example, ``ITALIC_BLUE``.
 * ``UNDERLINE_`` is a prefix qualifier that also may be used with any
   foreground color. For example, ``UNDERLINE_GREEN``.
-* Or any other combination of qualifiers, such as
+* ``SLOWBLINK_`` is a prefix modifier makes the text blink, slowly.
+  For example, ``SLOWBLINK_PURPLE``.
+* ``FASTBLINK_`` is a prefix modifier makes the text blink, quickly.
+  For example, ``FASTBLINK_CYAN``.
+* ``INVERT_`` is a prefix modifier swaps the foreground and background colors.
+  For example, ``INVERT_WHITE``.
+* ``CONCEAL_`` is a prefix modifier which hides the text. This may not be
+  widely supported. For example, ``CONCEAL_BLACK``.
+* ``STRIKETHROUGH_`` is a prefix modifier which draws a line through the text.
+  For example, ``STRIKETHROUGH_RED``.
+* Or any other combination of modifiers, such as
   ``BOLD_UNDERLINE_INTENSE_BLACK``,   which is the most metal color you
   can use!
 
+.. note:: Not all color modifier comboninations have been implemented on prompt-toolkit.
+
 You can make use of additional variables beyond these by adding them to the
 ``PROMPT_FIELDS`` environment variable.  The values in this dictionary
 should be strings (which will be inserted into the prompt verbatim), or
diff --git a/news/bad-colors.rst b/news/bad-colors.rst
new file mode 100644
index 00000000..188bf8f6
--- /dev/null
+++ b/news/bad-colors.rst
@@ -0,0 +1,38 @@
+**Added:**
+
+* Support for more ANSI escape sequece modifers allowed in color names.
+  The current modifiers now allowed are: BOLD, FAINT, ITALIC, UNDERLINE,
+  SLOWBLINK, FASTBLINK, INVERT, CONCEAL, and STRIKETHROUGH.
+* New ``ansi_tools.ansi_color_name_to_escape_code()`` function for
+  converting a color name to an ANSI escape code.
+* ``color_tools.RE_XONSH_COLOR`` is a regular expression for matching
+  xonsh color names.
+* ``color_tools.iscolor()`` is a simple function for testing whether a
+  string is a valid color name or not.
+* The ``tools.all_permutations()`` function yeilds all possible permutations
+  of an iterable, including removals.
+
+**Changed:**
+
+* ANSI color styles may now defined simply by their plain and intense colors.
+* ``SET_FOREGROUND_3INTS_`` renamed to ``SET_FOREGROUND_FAINT_``,
+  ``SET_BACKGROUND_3INTS_`` renamed to ``SET_BACKGROUND_FAINT_``,
+  ``SET_FOREGROUND_SHORT_`` renamed to ``SET_FOREGROUND_SLOWBLINK_``, and
+  ``SET_BACKGROUND_SHORT_`` renamed to ``SET_BACKGROUND_SLOWBLINK_``.
+
+**Deprecated:**
+
+* <news item>
+
+**Removed:**
+
+* ``ansi_tools.ANSI_REVERSE_COLOR_NAME_TRANSLATIONS`` removed, as it is
+  no longer needed.
+
+**Fixed:**
+
+* Fixed issues where ``$LS_COLORS`` could not convert valid ANSI colors.
+
+**Security:**
+
+* <news item>
diff --git a/xonsh/ansi_colors.py b/xonsh/ansi_colors.py
index 3711ccd9..45a913fa 100644
--- a/xonsh/ansi_colors.py
+++ b/xonsh/ansi_colors.py
@@ -7,17 +7,97 @@ import builtins
 from xonsh.platform import HAS_PYGMENTS
 from xonsh.lazyasd import LazyDict, lazyobject
 from xonsh.color_tools import (
-    RE_BACKGROUND,
+    RE_XONSH_COLOR,
     BASE_XONSH_COLORS,
     make_palette,
     find_closest_color,
     rgb2short,
     rgb_to_256,
     short_to_ints,
+    iscolor,
 )
 from xonsh.tools import FORMATTER
 
 
+def _ensure_color_map(style="default", cmap=None):
+    if cmap is not None:
+        pass
+    elif style in ANSI_STYLES:
+        cmap = ANSI_STYLES[style]
+    else:
+        try:  # dynamically loading the style
+            cmap = ansi_style_by_name(style)
+        except Exception:
+            msg = "Could not find color style {0!r}, using default."
+            print(msg.format(style), file=sys.stderr)
+            builtins.__xonsh__.env["XONSH_COLOR_STYLE"] = "default"
+            cmap = ANSI_STYLES["default"]
+    return cmap
+
+
+@lazyobject
+def ANSI_ESCAPE_MODIFIERS():
+    return {
+        "BOLD": "1",
+        "FAINT": "2",
+        "ITALIC": "3",
+        "UNDERLINE": "4",
+        "SLOWBLINK": "5",
+        "FASTBLINK": "6",
+        "INVERT": "7",
+        "CONCEAL": "8",
+        "STRIKETHROUGH": "9",
+    }
+
+
+def ansi_color_name_to_escape_code(name, style="default", cmap=None):
+    """Converts a color name to the inner part of an ANSI escape code"""
+    cmap = _ensure_color_map(style=style, cmap=cmap)
+    if name in cmap:
+        return cmap[name]
+    m = RE_XONSH_COLOR.match(name)
+    if m is None:
+        raise ValueError("{!r} is not a color!".format(name))
+    parts = m.groupdict()
+    # convert regex match into actual ANSI colors
+    if parts["nocolor"] is not None:
+        res = "0"
+    elif parts["bghex"] is not None:
+        res = "48;5;" + rgb_to_256(parts["bghex"][3:])[0]
+    elif parts["background"] is not None:
+        color = parts["color"]
+        if "#" in color:
+            res = "48;5;" + rgb_to_256(color[1:])[0]
+        else:
+            fgcolor = cmap[color]
+            if fgcolor.isdecimal():
+                res = str(int(fgcolor) + 10)
+            elif fgcolor.startswith("38;"):
+                res = "4" + fgcolor[1:]
+            else:
+                msg = (
+                    "when converting {!r}, did not recognize {!r} within "
+                    "the following color map as a valid color:\n\n{!r}"
+                )
+                raise ValueError(msg.format(name, fgcolor, cmap))
+    else:
+        # have regular, non-background color
+        mods = parts["modifiers"]
+        if mods is None:
+            mods = []
+        else:
+            mods = mods.strip("_").split("_")
+            mods = [ANSI_ESCAPE_MODIFIERS[mod] for mod in mods]
+        color = parts["color"]
+        if "#" in color:
+            mods.append("38;5;" + rgb_to_256(color[1:])[0])
+        else:
+            mods.append(cmap[color])
+        res = ";".join(mods)
+    cmap[name] = res
+    return res
+
+
 def ansi_partial_color_format(template, style="default", cmap=None, hide=False):
     """Formats a template string but only with respect to the colors.
     Another template string is returned, with the color values filled in.
@@ -49,18 +129,7 @@ def ansi_partial_color_format(template, style="default", cmap=None, hide=False):
 
 
 def _ansi_partial_color_format_main(template, style="default", cmap=None, hide=False):
-    if cmap is not None:
-        pass
-    elif style in ANSI_STYLES:
-        cmap = ANSI_STYLES[style]
-    else:
-        try:  # dynamically loading the style
-            cmap = ansi_style_by_name(style)
-        except Exception:
-            msg = "Could not find color style {0!r}, using default."
-            print(msg.format(style), file=sys.stderr)
-            builtins.__xonsh__.env["XONSH_COLOR_STYLE"] = "default"
-            cmap = ANSI_STYLES["default"]
+    cmap = _ensure_color_map(style=style, cmap=cmap)
     esc = ("\001" if hide else "") + "\033["
     m = "m" + ("\002" if hide else "")
     bopen = "{"
@@ -74,18 +143,9 @@ def _ansi_partial_color_format_main(template, style="default", cmap=None, hide=F
             pass
         elif field in cmap:
             toks.extend([esc, cmap[field], m])
-        elif "#" in field:
-            field = field.lower()
-            pre, _, post = field.partition("#")
-            f_or_b = "38" if RE_BACKGROUND.search(pre) is None else "48"
-            rgb, _, post = post.partition("_")
-            c256, _ = rgb_to_256(rgb)
-            color = f_or_b + ";5;" + c256
-            mods = pre + "_" + post
-            if "underline" in mods:
-                color = "4;" + color
-            if "bold" in mods:
-                color = "1;" + color
+        elif iscolor(field):
+            color = ansi_color_name_to_escape_code(field, cmap=cmap)
+            cmap[field] = color
             toks.extend([esc, color, m])
         elif field is not None:
             toks.append(bopen)
@@ -127,16 +187,19 @@ def ansi_reverse_style(style="default", return_style=False):
     updates = {
         "1": "BOLD_",
         "2": "FAINT_",
+        "3": "ITALIC_",
         "4": "UNDERLINE_",
         "5": "SLOWBLINK_",
-        "1;4": "BOLD_UNDERLINE_",
-        "4;1": "BOLD_UNDERLINE_",
+        "6": "FASTBLINK_",
+        "7": "INVERT_",
+        "8": "CONCEAL_",
+        "9": "STRIKETHROUGH_",
         "38": "SET_FOREGROUND_",
         "48": "SET_BACKGROUND_",
-        "38;2": "SET_FOREGROUND_3INTS_",
-        "48;2": "SET_BACKGROUND_3INTS_",
-        "38;5": "SET_FOREGROUND_SHORT_",
-        "48;5": "SET_BACKGROUND_SHORT_",
+        "38;2": "SET_FOREGROUND_FAINT_",
+        "48;2": "SET_BACKGROUND_FAINT_",
+        "38;5": "SET_FOREGROUND_SLOWBLINK_",
+        "48;5": "SET_BACKGROUND_SLOWBLINK_",
     }
     for ec, name in reversed_style.items():
         no_left_zero = ec.lstrip("0")
@@ -157,31 +220,14 @@ def ANSI_ESCAPE_CODE_RE():
     return re.compile(r"\001?(\033\[)?([0-9;]+)m?\002?")
 
 
-@lazyobject
-def ANSI_REVERSE_COLOR_NAME_TRANSLATIONS():
-    base = {
-        "SET_FOREGROUND_FAINT_": "SET_FOREGROUND_3INTS_",
-        "SET_BACKGROUND_FAINT_": "SET_BACKGROUND_3INTS_",
-        "SET_FOREGROUND_SLOWBLINK_": "SET_FOREGROUND_SHORT_",
-        "SET_BACKGROUND_SLOWBLINK_": "SET_BACKGROUND_SHORT_",
-    }
-    data = {"UNDERLINE_BOLD_": "BOLD_UNDERLINE_"}
-    data.update(base)
-    data.update({"BOLD_" + k: "BOLD_" + v for k, v in base.items()})
-    data.update({"UNDERLINE_" + k: "UNDERLINE_" + v for k, v in base.items()})
-    data.update({"BOLD_UNDERLINE_" + k: "BOLD_UNDERLINE_" + v for k, v in base.items()})
-    data.update({"UNDERLINE_BOLD_" + k: "BOLD_UNDERLINE_" + v for k, v in base.items()})
-    return data
-
-
 @lazyobject
 def ANSI_COLOR_NAME_SET_3INTS_RE():
-    return re.compile(r"(\w+_)?SET_(FORE|BACK)GROUND_3INTS_(\d+)_(\d+)_(\d+)")
+    return re.compile(r"(\w+_)?SET_(FORE|BACK)GROUND_FAINT_(\d+)_(\d+)_(\d+)")
 
 
 @lazyobject
 def ANSI_COLOR_NAME_SET_SHORT_RE():
-    return re.compile(r"(\w+_)?SET_(FORE|BACK)GROUND_SHORT_(\d+)")
+    return re.compile(r"(\w+_)?SET_(FORE|BACK)GROUND_SLOWBLINK_(\d+)")
 
 
 def _color_name_from_ints(ints, background=False, prefix=None):
@@ -234,13 +280,15 @@ def ansi_color_escape_code_to_name(escape_code, style, reversed_style=None):
     # normalize names
     n = ""
     norm_names = []
-    colors = set(reversed_style.values())
+    prefixes = ""
     for name in names:
         if name == "NO_COLOR":
             # skip most '0' entries
             continue
+        elif "BACKGROUND_" in name and n:
+            prefixes += n
+            n = ""
         n = n + name if n else name
-        n = ANSI_REVERSE_COLOR_NAME_TRANSLATIONS.get(n, n)
         if n.endswith("_"):
             continue
         elif ANSI_COLOR_NAME_SET_SHORT_RE.match(n) is not None:
@@ -253,12 +301,12 @@ def ansi_color_escape_code_to_name(escape_code, style, reversed_style=None):
             n = _color_name_from_ints(
                 (int(r), int(g), int(b)), background=(fore_back == "BACK"), prefix=pre
             )
-        elif "GROUND_3INTS_" in n:
+        elif "GROUND_FAINT_" in n:
             # have 1 or 2, but not 3 ints
             n += "_"
             continue
         # error check
-        if n not in colors:
+        if not iscolor(n):
             msg = (
                 "Could not translate ANSI color code {escape_code!r} "
                 "into a known color in the palette. Specifically, the {n!r} "
@@ -269,6 +317,16 @@ def ansi_color_escape_code_to_name(escape_code, style, reversed_style=None):
             )
         norm_names.append(n)
         n = ""
+    # check if we have pre- & post-fixes to apply to the last, non-background element
+    prefixes += n
+    if prefixes.endswith("_"):
+        for i in range(-1, -len(norm_names) - 1, -1):
+            if "BACKGROUND_" not in norm_names[i]:
+                norm_names[i] = prefixes + norm_names[i]
+                break
+        else:
+            # only have background colors, so select WHITE as default color
+            norm_names.append(prefixes + "WHITE")
     # return
     if len(norm_names) == 0:
         return ("NO_COLOR",)
@@ -276,23 +334,6 @@ def ansi_color_escape_code_to_name(escape_code, style, reversed_style=None):
         return tuple(norm_names)
 
 
-def _ansi_expand_style(cmap):
-    """Expands a style in order to more quickly make color map changes."""
-    for key, val in list(cmap.items()):
-        if key == "NO_COLOR":
-            continue
-        elif len(val) == 0:
-            cmap["BOLD_" + key] = "1"
-            cmap["UNDERLINE_" + key] = "4"
-            cmap["BOLD_UNDERLINE_" + key] = "1;4"
-            cmap["BACKGROUND_" + key] = val
-        else:
-            cmap["BOLD_" + key] = "1;" + val
-            cmap["UNDERLINE_" + key] = "4;" + val
-            cmap["BOLD_UNDERLINE_" + key] = "1;4;" + val
-            cmap["BACKGROUND_" + key] = val.replace("38", "48", 1)
-
-
 def _bw_style():
     style = {
         "BLACK": "0;30",
@@ -313,7 +354,6 @@ def _bw_style():
         "WHITE": "0;37",
         "YELLOW": "0;37",
     }
-    _ansi_expand_style(style)
     return style
 
 
@@ -330,33 +370,6 @@ def _default_style():
         "PURPLE": "0;35",  # PURPLE
         "CYAN": "0;36",  # CYAN
         "WHITE": "0;37",  # WHITE
-        # Bold
-        "BOLD_BLACK": "1;30",  # BLACK
-        "BOLD_RED": "1;31",  # RED
-        "BOLD_GREEN": "1;32",  # GREEN
-        "BOLD_YELLOW": "1;33",  # YELLOW
-        "BOLD_BLUE": "1;34",  # BLUE
-        "BOLD_PURPLE": "1;35",  # PURPLE
-        "BOLD_CYAN": "1;36",  # CYAN
-        "BOLD_WHITE": "1;37",  # WHITE
-        # Underline
-        "UNDERLINE_BLACK": "4;30",  # BLACK
-        "UNDERLINE_RED": "4;31",  # RED
-        "UNDERLINE_GREEN": "4;32",  # GREEN
-        "UNDERLINE_YELLOW": "4;33",  # YELLOW
-        "UNDERLINE_BLUE": "4;34",  # BLUE
-        "UNDERLINE_PURPLE": "4;35",  # PURPLE
-        "UNDERLINE_CYAN": "4;36",  # CYAN
-        "UNDERLINE_WHITE": "4;37",  # WHITE
-        # Bold, Underline
-        "BOLD_UNDERLINE_BLACK": "1;4;30",  # BLACK
-        "BOLD_UNDERLINE_RED": "1;4;31",  # RED
-        "BOLD_UNDERLINE_GREEN": "1;4;32",  # GREEN
-        "BOLD_UNDERLINE_YELLOW": "1;4;33",  # YELLOW
-        "BOLD_UNDERLINE_BLUE": "1;4;34",  # BLUE
-        "BOLD_UNDERLINE_PURPLE": "1;4;35",  # PURPLE
-        "BOLD_UNDERLINE_CYAN": "1;4;36",  # CYAN
-        "BOLD_UNDERLINE_WHITE": "1;4;37",  # WHITE
         # Background
         "BACKGROUND_BLACK": "40",  # BLACK
         "BACKGROUND_RED": "41",  # RED
@@ -375,33 +388,6 @@ def _default_style():
         "INTENSE_PURPLE": "0;95",  # PURPLE
         "INTENSE_CYAN": "0;96",  # CYAN
         "INTENSE_WHITE": "0;97",  # WHITE
-        # Bold High Intensity
-        "BOLD_INTENSE_BLACK": "1;90",  # BLACK
-        "BOLD_INTENSE_RED": "1;91",  # RED
-        "BOLD_INTENSE_GREEN": "1;92",  # GREEN
-        "BOLD_INTENSE_YELLOW": "1;93",  # YELLOW
-        "BOLD_INTENSE_BLUE": "1;94",  # BLUE
-        "BOLD_INTENSE_PURPLE": "1;95",  # PURPLE
-        "BOLD_INTENSE_CYAN": "1;96",  # CYAN
-        "BOLD_INTENSE_WHITE": "1;97",  # WHITE
-        # Underline High Intensity
-        "UNDERLINE_INTENSE_BLACK": "4;90",  # BLACK
-        "UNDERLINE_INTENSE_RED": "4;91",  # RED
-        "UNDERLINE_INTENSE_GREEN": "4;92",  # GREEN
-        "UNDERLINE_INTENSE_YELLOW": "4;93",  # YELLOW
-        "UNDERLINE_INTENSE_BLUE": "4;94",  # BLUE
-        "UNDERLINE_INTENSE_PURPLE": "4;95",  # PURPLE
-        "UNDERLINE_INTENSE_CYAN": "4;96",  # CYAN
-        "UNDERLINE_INTENSE_WHITE": "4;97",  # WHITE
-        # Bold Underline High Intensity
-        "BOLD_UNDERLINE_INTENSE_BLACK": "1;4;90",  # BLACK
-        "BOLD_UNDERLINE_INTENSE_RED": "1;4;91",  # RED
-        "BOLD_UNDERLINE_INTENSE_GREEN": "1;4;92",  # GREEN
-        "BOLD_UNDERLINE_INTENSE_YELLOW": "1;4;93",  # YELLOW
-        "BOLD_UNDERLINE_INTENSE_BLUE": "1;4;94",  # BLUE
-        "BOLD_UNDERLINE_INTENSE_PURPLE": "1;4;95",  # PURPLE
-        "BOLD_UNDERLINE_INTENSE_CYAN": "1;4;96",  # CYAN
-        "BOLD_UNDERLINE_INTENSE_WHITE": "1;4;97",  # WHITE
         # High Intensity backgrounds
         "BACKGROUND_INTENSE_BLACK": "0;100",  # BLACK
         "BACKGROUND_INTENSE_RED": "0;101",  # RED
@@ -435,7 +421,6 @@ def _monokai_style():
         "INTENSE_WHITE": "38;5;15",
         "INTENSE_YELLOW": "38;5;186",
     }
-    _ansi_expand_style(style)
     return style
 
 
@@ -464,7 +449,6 @@ def _algol_style():
         "WHITE": "38;5;102",
         "YELLOW": "38;5;09",
     }
-    _ansi_expand_style(style)
     return style
 
 
@@ -488,7 +472,6 @@ def _algol_nu_style():
         "WHITE": "38;5;102",
         "YELLOW": "38;5;09",
     }
-    _ansi_expand_style(style)
     return style
 
 
@@ -512,7 +495,6 @@ def _autumn_style():
         "WHITE": "38;5;145",
         "YELLOW": "38;5;130",
     }
-    _ansi_expand_style(style)
     return style
 
 
@@ -536,7 +518,6 @@ def _borland_style():
         "WHITE": "38;5;145",
         "YELLOW": "38;5;124",
     }
-    _ansi_expand_style(style)
     return style
 
 
@@ -560,7 +541,6 @@ def _colorful_style():
         "WHITE": "38;5;145",
         "YELLOW": "38;5;130",
     }
-    _ansi_expand_style(style)
     return style
 
 
@@ -584,7 +564,6 @@ def _emacs_style():
         "WHITE": "38;5;145",
         "YELLOW": "38;5;130",
     }
-    _ansi_expand_style(style)
     return style
 
 
@@ -608,7 +587,6 @@ def _friendly_style():
         "WHITE": "38;5;145",
         "YELLOW": "38;5;166",
     }
-    _ansi_expand_style(style)
     return style
 
 
@@ -632,7 +610,6 @@ def _fruity_style():
         "WHITE": "38;5;187",
         "YELLOW": "38;5;202",
     }
-    _ansi_expand_style(style)
     return style
 
 
@@ -656,7 +633,6 @@ def _igor_style():
         "WHITE": "38;5;163",
         "YELLOW": "38;5;166",
     }
-    _ansi_expand_style(style)
     return style
 
 
@@ -680,7 +656,6 @@ def _lovelace_style():
         "WHITE": "38;5;102",
         "YELLOW": "38;5;130",
     }
-    _ansi_expand_style(style)
     return style
 
 
@@ -704,7 +679,6 @@ def _manni_style():
         "WHITE": "38;5;145",
         "YELLOW": "38;5;166",
     }
-    _ansi_expand_style(style)
     return style
 
 
@@ -728,7 +702,6 @@ def _murphy_style():
         "WHITE": "38;5;145",
         "YELLOW": "38;5;166",
     }
-    _ansi_expand_style(style)
     return style
 
 
@@ -752,7 +725,6 @@ def _native_style():
         "WHITE": "38;5;145",
         "YELLOW": "38;5;124",
     }
-    _ansi_expand_style(style)
     return style
 
 
@@ -776,7 +748,6 @@ def _paraiso_dark_style():
         "WHITE": "38;5;79",
         "YELLOW": "38;5;214",
     }
-    _ansi_expand_style(style)
     return style
 
 
@@ -800,7 +771,6 @@ def _paraiso_light_style():
         "WHITE": "38;5;102",
         "YELLOW": "38;5;214",
     }
-    _ansi_expand_style(style)
     return style
 
 
@@ -824,7 +794,6 @@ def _pastie_style():
         "WHITE": "38;5;145",
         "YELLOW": "38;5;130",
     }
-    _ansi_expand_style(style)
     return style
 
 
@@ -848,7 +817,6 @@ def _perldoc_style():
         "WHITE": "38;5;145",
         "YELLOW": "38;5;166",
     }
-    _ansi_expand_style(style)
     return style
 
 
@@ -872,7 +840,6 @@ def _rrt_style():
         "WHITE": "38;5;117",
         "YELLOW": "38;5;09",
     }
-    _ansi_expand_style(style)
     return style
 
 
@@ -896,7 +863,6 @@ def _tango_style():
         "WHITE": "38;5;15",
         "YELLOW": "38;5;94",
     }
-    _ansi_expand_style(style)
     return style
 
 
@@ -920,7 +886,6 @@ def _trac_style():
         "WHITE": "38;5;145",
         "YELLOW": "38;5;100",
     }
-    _ansi_expand_style(style)
     return style
 
 
@@ -944,7 +909,6 @@ def _vim_style():
         "WHITE": "38;5;188",
         "YELLOW": "38;5;160",
     }
-    _ansi_expand_style(style)
     return style
 
 
@@ -968,7 +932,6 @@ def _vs_style():
         "WHITE": "38;5;31",
         "YELLOW": "38;5;124",
     }
-    _ansi_expand_style(style)
     return style
 
 
@@ -992,7 +955,6 @@ def _xcode_style():
         "WHITE": "38;5;60",
         "YELLOW": "38;5;94",
     }
-    _ansi_expand_style(style)
     return style
 
 
@@ -1071,10 +1033,6 @@ def make_ansi_style(palette):
             closest = "".join([a * 2 for a in closest])
         short = rgb2short(closest)[0]
         style[name] = "38;5;" + short
-        style["BOLD_" + name] = "1;38;5;" + short
-        style["UNDERLINE_" + name] = "4;38;5;" + short
-        style["BOLD_UNDERLINE_" + name] = "1;4;38;5;" + short
-        style["BACKGROUND_" + name] = "48;5;" + short
     return style
 
 
diff --git a/xonsh/color_tools.py b/xonsh/color_tools.py
index 1a605842..d50560e1 100644
--- a/xonsh/color_tools.py
+++ b/xonsh/color_tools.py
@@ -38,23 +38,52 @@ def BASE_XONSH_COLORS():
     }
 
 
+@lazyobject
+def RE_XONSH_COLOR():
+    hex = "[0-9a-fA-F]"
+    s = (
+        # background
+        r"((?P<background>BACKGROUND_)|(?P<modifiers>("
+        # modifiers, only apply to foreground
+        r"BOLD_|FAINT_|ITALIC_|UNDERLINE_|SLOWBLINK_|FASTBLINK_|INVERT_|CONCEAL_|"
+        r"STRIKETHROUGH_)+))?"
+        # colors
+        r"(?P<color>BLACK|RED|GREEN|YELLOW|BLUE|PURPLE|CYAN|WHITE|INTENSE_BLACK|"
+        r"INTENSE_RED|INTENSE_GREEN|INTENSE_YELLOW|INTENSE_BLUE|INTENSE_PURPLE|"
+        r"INTENSE_CYAN|INTENSE_WHITE|#" + hex + "{3}|#" + hex + "{6})"
+    )
+    bghex = (
+        "bg#" + hex + "{3}|"
+        "bg#" + hex + "{6}|"
+        "BG#" + hex + "{3}|"
+        "BG#" + hex + "{6}"
+    )
+    s = "^((?P<nocolor>NO_COLOR)|(?P<bghex>" + bghex + ")|" + s + ")$"
+    return re.compile(s)
+
+
+def iscolor(s):
+    """Tests if a string is a valid color"""
+    return RE_XONSH_COLOR.match(s) is not None
+
+
 @lazyobject
 def CLUT():
     """color look-up table"""
     return [
         #    8-bit, RGB hex
         # Primary 3-bit (8 colors). Unique representation!
-        ("00", "000000"),
-        ("01", "800000"),
-        ("02", "008000"),
-        ("03", "808000"),
-        ("04", "000080"),
-        ("05", "800080"),
-        ("06", "008080"),
-        ("07", "c0c0c0"),
+        ("0", "000000"),
+        ("1", "800000"),
+        ("2", "008000"),
+        ("3", "808000"),
+        ("4", "000080"),
+        ("5", "800080"),
+        ("6", "008080"),
+        ("7", "c0c0c0"),
         # Equivalent "bright" versions of original 8 colors.
-        ("08", "808080"),
-        ("09", "ff0000"),
+        ("8", "808080"),
+        ("9", "ff0000"),
         ("10", "00ff00"),
         ("11", "ffff00"),
         ("12", "0000ff"),
@@ -347,7 +376,8 @@ def rgb_to_256(rgb):
 
     Returns
     -------
-    String between 0 and 255, compatible with xterm.
+    Tuple of String between 0 and 255 (compatible with xterm) and
+    hex code (length-6).
     """
     rgb = rgb.lstrip("#")
     if len(rgb) == 0:
diff --git a/xonsh/environ.py b/xonsh/environ.py
index 0c4d3806..0d5671f6 100644
--- a/xonsh/environ.py
+++ b/xonsh/environ.py
@@ -82,6 +82,7 @@ from xonsh.tools import (
 )
 from xonsh.ansi_colors import (
     ansi_color_escape_code_to_name,
+    ansi_color_name_to_escape_code,
     ansi_reverse_style,
     ansi_style_by_name,
 )
@@ -364,7 +365,11 @@ class LsColors(cabc.MutableMapping):
         if self._detyped is None:
             self._detyped = ":".join(
                 [
-                    key + "=" + ";".join([style[v] or "0" for v in val])
+                    key
+                    + "="
+                    + ";".join(
+                        [ansi_color_name_to_escape_code(v, cmap=style) for v in val]
+                    )
                     for key, val in sorted(self._d.items())
                 ]
             )
diff --git a/xonsh/tools.py b/xonsh/tools.py
index 98e3531d..59d43370 100644
--- a/xonsh/tools.py
+++ b/xonsh/tools.py
@@ -1713,6 +1713,12 @@ def history_tuple_to_str(x):
     return "{0} {1}".format(*x)
 
 
+def all_permutations(iterable):
+    """Yeilds all permutations, not just those of a specified length"""
+    for r in range(1, len(iterable) + 1):
+        yield from itertools.permutations(iterable, r=r)
+
+
 def format_color(string, **kwargs):
     """Formats strings that may contain colors. This simply dispatches to the
     shell instances method of the same name. The results of this function should


Test patch:
diff --git a/tests/test_ansi_colors.py b/tests/test_ansi_colors.py
index 8875a25b..8a907a4a 100644
--- a/tests/test_ansi_colors.py
+++ b/tests/test_ansi_colors.py
@@ -1,7 +1,71 @@
 """Tests ANSI color tools."""
 import pytest
 
-from xonsh.ansi_colors import ansi_color_escape_code_to_name, ansi_reverse_style
+from xonsh.ansi_colors import (
+    ansi_color_escape_code_to_name,
+    ansi_reverse_style,
+    ansi_color_name_to_escape_code,
+)
+
+
+DEFAULT_CMAP = {
+    # Reset
+    "NO_COLOR": "0",  # Text Reset
+    # Regular Colors
+    "BLACK": "0;30",  # BLACK
+    "RED": "0;31",  # RED
+    "GREEN": "0;32",  # GREEN
+    "YELLOW": "0;33",  # YELLOW
+    "BLUE": "0;34",  # BLUE
+    "PURPLE": "0;35",  # PURPLE
+    "CYAN": "0;36",  # CYAN
+    "WHITE": "0;37",  # WHITE
+    # Background
+    "BACKGROUND_BLACK": "40",  # BLACK
+    "BACKGROUND_RED": "41",  # RED
+    "BACKGROUND_GREEN": "42",  # GREEN
+    "BACKGROUND_YELLOW": "43",  # YELLOW
+    "BACKGROUND_BLUE": "44",  # BLUE
+    "BACKGROUND_PURPLE": "45",  # PURPLE
+    "BACKGROUND_CYAN": "46",  # CYAN
+    "BACKGROUND_WHITE": "47",  # WHITE
+    # High Intensity
+    "INTENSE_BLACK": "90",  # BLACK
+    "INTENSE_RED": "91",  # RED
+    "INTENSE_GREEN": "92",  # GREEN
+    "INTENSE_YELLOW": "93",  # YELLOW
+    "INTENSE_BLUE": "94",  # BLUE
+    "INTENSE_PURPLE": "95",  # PURPLE
+    "INTENSE_CYAN": "96",  # CYAN
+    "INTENSE_WHITE": "97",  # WHITE
+}
+
+
+@pytest.mark.parametrize(
+    "name, exp",
+    [
+        ("NO_COLOR", "0"),
+        ("RED", "0;31"),
+        ("BACKGROUND_RED", "41"),
+        ("BACKGROUND_INTENSE_RED", "101"),
+        ("BOLD_RED", "1;0;31"),
+        ("UNDERLINE_RED", "4;0;31"),
+        ("BOLD_UNDERLINE_RED", "1;4;0;31"),
+        ("UNDERLINE_BOLD_RED", "4;1;0;31"),
+        # The hex code #000 can map to ANSI-256 0 or 16
+        ("#000", {"38;5;0", "38;5;16"}),
+        ("#000000", {"38;5;0", "38;5;16"}),
+        ("BACKGROUND_#000", {"48;5;0", "48;5;16"}),
+        ("BACKGROUND_#000000", {"48;5;0", "48;5;16"}),
+        ("BG#000", {"48;5;0", "48;5;16"}),
+        ("bg#000000", {"48;5;0", "48;5;16"}),
+    ],
+)
+def test_ansi_color_name_to_escape_code_default(name, exp):
+    cmap = DEFAULT_CMAP.copy()
+    obs = ansi_color_name_to_escape_code(name, cmap=cmap)
+    assert obs in exp
+
 
 RS = ansi_reverse_style(style="default")
 
@@ -16,6 +80,7 @@ def test_ansi_reverse_style(key, value):
     "inp, exp",
     [
         ("0", ("NO_COLOR",)),
+        ("1", ("BOLD_WHITE",)),
         ("\0010\002", ("NO_COLOR",)),
         ("\033[0m", ("NO_COLOR",)),
         ("\001\033[0m\002", ("NO_COLOR",)),
@@ -23,7 +88,7 @@ def test_ansi_reverse_style(key, value):
         ("01;31", ("BOLD_RED",)),
         ("04;31", ("UNDERLINE_RED",)),
         ("1;4;31", ("BOLD_UNDERLINE_RED",)),
-        ("4;1;31", ("BOLD_UNDERLINE_RED",)),
+        ("4;1;31", ("UNDERLINE_BOLD_RED",)),
         ("31;42", ("RED", "BACKGROUND_GREEN")),
         ("42;31", ("BACKGROUND_GREEN", "RED")),
         ("40", ("BACKGROUND_BLACK",)),
@@ -32,9 +97,23 @@ def test_ansi_reverse_style(key, value):
         ("38;2;170;0;0", ("RED",)),
         ("48;2;170;0;0", ("BACKGROUND_RED",)),
         ("1;38;5;124", ("BOLD_RED",)),
-        ("4;1;38;2;170;0;0", ("BOLD_UNDERLINE_RED",)),
+        ("4;1;38;2;170;0;0", ("UNDERLINE_BOLD_RED",)),
         ("1;38;5;40", ("BOLD_GREEN",)),
         ("48;5;16;38;5;184", ("BACKGROUND_BLACK", "INTENSE_YELLOW")),
+        ("01;05;37;41", ("BOLD_SLOWBLINK_WHITE", "BACKGROUND_RED")),
+        ("38;5;113;1", ("BOLD_INTENSE_GREEN",)),
+        ("48;5;196;38;5;232;1", ("BACKGROUND_RED", "BOLD_BLACK")),
+        ("48;5;3;38;5;0", ("BACKGROUND_YELLOW", "BLACK")),
+        (
+            "38;5;220;1;3;100",
+            ("BOLD_ITALIC_INTENSE_YELLOW", "BACKGROUND_INTENSE_BLACK"),
+        ),
+        (
+            "38;5;220;1;3;100;1",
+            ("BOLD_ITALIC_BOLD_INTENSE_YELLOW", "BACKGROUND_INTENSE_BLACK"),
+        ),
+        ("48;5;235;38;5;139;3", ("BACKGROUND_BLACK", "ITALIC_WHITE")),
+        ("38;5;111;4", ("UNDERLINE_WHITE",)),
     ],
 )
 def test_ansi_color_escape_code_to_name(inp, exp):
diff --git a/tests/test_color_tools.py b/tests/test_color_tools.py
new file mode 100644
index 00000000..ef8f1db9
--- /dev/null
+++ b/tests/test_color_tools.py
@@ -0,0 +1,45 @@
+"""Tests color tools."""
+import pytest
+
+from xonsh.color_tools import iscolor
+
+
+@pytest.mark.parametrize(
+    "inp, exp",
+    [
+        ("NO_COLOR", True),
+        ("CYAN", True),
+        ("PURPLE", True),
+        ("INTENSE_YELLOW", True),
+        ("#fff", True),
+        ("#FFF", True),
+        ("#fafad2", True),
+        ("#FAFAD2", True),
+        ("BOLD_RED", True),
+        ("BOLD_GREEN", True),
+        ("UNDERLINE_RED", True),
+        ("BOLD_UNDERLINE_RED", True),
+        ("UNDERLINE_BOLD_RED", True),
+        ("BACKGROUND_RED", True),
+        ("BACKGROUND_GREEN", True),
+        ("BACKGROUND_BLACK", True),
+        ("BACKGROUND_PURPLE", True),
+        ("BACKGROUND_INTENSE_RED", True),
+        ("BACKGROUND_#123456", True),
+        ("bg#fff", True),
+        ("bg#fafad2", True),
+        ("BG#fff", True),
+        ("BG#fafad2", True),
+        ("WAKKA", False),
+        ("#F", False),
+        ("#FAFAD", False),
+        ("UNDERLINE_BACKGROUND_RED", False),
+        ("BACKGROUND_BOLD_RED", False),
+    ],
+)
+def test_iscolor(inp, exp):
+    obs = iscolor(inp)
+    if exp:
+        assert obs
+    else:
+        assert not obs
diff --git a/tests/test_environ.py b/tests/test_environ.py
index f770e1f4..3469e20e 100644
--- a/tests/test_environ.py
+++ b/tests/test_environ.py
@@ -68,7 +68,7 @@ def test_env_detype_mutable_access_clear(path1, path2):
 
 def test_env_detype_no_dict():
     env = Env(YO={"hey": 42})
-    env.set_ensurer('YO', Ensurer(always_true, None, None))
+    env.set_ensurer("YO", Ensurer(always_true, None, None))
     det = env.detype()
     assert "YO" not in det
 
diff --git a/tests/test_tools.py b/tests/test_tools.py
index 5458cffb..df69c289 100644
--- a/tests/test_tools.py
+++ b/tests/test_tools.py
@@ -72,6 +72,7 @@ from xonsh.tools import (
     is_writable_file,
     balanced_parens,
     iglobpath,
+    all_permutations,
 )
 from xonsh.environ import Env
 
@@ -1676,3 +1677,25 @@ def test_iglobpath_empty_str(monkeypatch, xonsh_builtins):
     monkeypatch.setattr(os, "listdir", mocklistdir)
     paths = list(iglobpath("some/path"))
     assert len(paths) == 0
+
+
+def test_all_permutations():
+    obs = {"".join(p) for p in all_permutations("ABC")}
+    exp = {
+        "A",
+        "B",
+        "C",
+        "AB",
+        "AC",
+        "BA",
+        "BC",
+        "CA",
+        "CB",
+        "ACB",
+        "CBA",
+        "BAC",
+        "CAB",
+        "BCA",
+        "ABC",
+    }
+    assert obs == exp