Repository: xonsh/xonsh

Issue:
ValueError: 'TARGET' is not a color!
<!--- Provide a general summary of the issue in the Title above -->
<!--- If you have a question along the lines of "How do I do this Bash command in xonsh"
please first look over the Bash to Xonsh translation guide: https://xon.sh/bash_to_xsh.html
If you don't find an answer there, please do open an issue! -->

## xonfig

<details>

```
+------------------+---------------------+
| xonsh            | 0.9.18              |
| Git SHA          | 26475146            |
| Commit Date      | May 5 06:42:28 2020 |
| Python           | 3.7.7               |
| PLY              | 3.11                |
| have readline    | True                |
| prompt toolkit   | 3.0.5               |
| shell type       | prompt_toolkit      |
| pygments         | 2.6.1               |
| on posix         | True                |
| on linux         | True                |
| distro           | fedora              |
| on darwin        | False               |
| on windows       | False               |
| on cygwin        | False               |
| on msys2         | False               |
| is superuser     | False               |
| default encoding | utf-8               |
| xonsh encoding   | utf-8               |
| encoding errors  | surrogateescape     |
+------------------+---------------------+
```

</details>

## Expected Behavior
<!--- Tell us what should happen -->
Xonsh should support the target color in $LS_COLORS
## Current Behavior
<!--- Tell us what happens instead of the expected behavior -->
<!--- If part of your bug report is a traceback, please first enter debug mode before triggering the error
To enter debug mode, set the environment variable `XONSH_DEBUG=1` _before_ starting `xonsh`.
On Linux and OSX, an easy way to to do this is to run `env XONSH_DEBUG=1 xonsh` -->

### Traceback (if applicable)

<details>

```
Traceback (most recent call last):
  File /home/tim/.xonsh/lib64/python3.7/site-packages/xonsh/__amalgam__.py, line 24210, in main
    return main_xonsh(args)
  File /home/tim/.xonsh/lib64/python3.7/site-packages/xonsh/__amalgam__.py, line 24239, in main_xonsh
    shell.shell.cmdloop()
  File /home/tim/.xonsh/lib64/python3.7/site-packages/xonsh/ptk_shell/shell.py, line 197, in cmdloop
    line = self.singleline(auto_suggest=auto_suggest)
  File /home/tim/.xonsh/lib64/python3.7/site-packages/xonsh/ptk_shell/shell.py, line 101, in singleline
    self.styler.style_name = env.get(XONSH_COLOR_STYLE)
  File /home/tim/.xonsh/lib64/python3.7/site-packages/xonsh/__amalgam__.py, line 17732, in styler
    self._styler = XonshStyle(env.get(XONSH_COLOR_STYLE))
  File /home/tim/.xonsh/lib64/python3.7/site-packages/xonsh/pyghooks.py, line 345, in __init__
    self.style_name = style_name
  File /home/tim/.xonsh/lib64/python3.7/site-packages/xonsh/pyghooks.py, line 384, in style_name
    color_token = color_token_by_name(xonsh_color, self.styles)
  File /home/tim/.xonsh/lib64/python3.7/site-packages/xonsh/pyghooks.py, line 223, in color_token_by_name
    pc = color_name_to_pygments_code(xc[0], styles)
  File /home/tim/.xonsh/lib64/python3.7/site-packages/xonsh/pyghooks.py, line 143, in color_name_to_pygments_code
    if token in styles:
  File /usr/lib64/python3.7/collections/__init__.py, line 931, in __contains__
    return any(key in m for m in self.maps)
  File /usr/lib64/python3.7/collections/__init__.py, line 931, in <genexpr>
    return any(key in m for m in self.maps)
  File /usr/lib64/python3.7/_collections_abc.py, line 666, in __contains__
    self[key]
  File /home/tim/.xonsh/lib64/python3.7/site-packages/xonsh/pyghooks.py, line 312, in __getitem__
    value = code_by_name(name, self.styles)
  File /home/tim/.xonsh/lib64/python3.7/site-packages/xonsh/pyghooks.py, line 205, in code_by_name
    code = color_name_to_pygments_code(fg, styles)
  File /home/tim/.xonsh/lib64/python3.7/site-packages/xonsh/pyghooks.py, line 147, in color_name_to_pygments_code
    raise ValueError(!r is not a color!.format(name))
ValueError: 'TARGET' is not a color!
Xonsh encountered an issue during launch
Failback to /bin/sh
```

</details>

## Steps to Reproduce
<!--- Please try to write out a minimal reproducible snippet to trigger the bug, it will help us fix it! -->
- run `LS_COLORS='ln=target' xonsh` in bash


Discussion:
Thanks for reporting, @swedneck!
I can confirm the issue. It was supposed to be fixed by #3255, but apparently was not. 

Resolving patch:
diff --git a/xonsh/environ.py b/xonsh/environ.py
--- a/xonsh/environ.py
+++ b/xonsh/environ.py
@@ -198,8 +198,8 @@ def to_debug(x):
 
 class LsColors(cabc.MutableMapping):
     """Helps convert to/from $LS_COLORS format, respecting the xonsh color style.
-    This accepts the same inputs as dict(). The link ``target`` is represented
-    by the special ``"TARGET"`` color.
+    This accepts the same inputs as dict(). The special value ``target`` is
+    replaced by no color, but sets a flag for cognizant application (see is_target()).
     """
 
     default_settings = {
@@ -321,6 +321,7 @@ class LsColors(cabc.MutableMapping):
         "di": ("BOLD_BLUE",),
         "do": ("BOLD_PURPLE",),
         "ex": ("BOLD_GREEN",),
+        "fi": ("NO_COLOR",),
         "ln": ("BOLD_CYAN",),
         "mh": ("NO_COLOR",),
         "mi": ("NO_COLOR",),
@@ -335,10 +336,20 @@ class LsColors(cabc.MutableMapping):
         "tw": ("BLACK", "BACKGROUND_GREEN"),
     }
 
-    def __init__(self, *args, **kwargs):
-        self._d = dict(*args, **kwargs)
+    target_value = "target"  # special value to set for ln=target
+    target_color = ("NO_COLOR",)  # repres in color space
+
+    def __init__(self, ini_dict: dict = None):
         self._style = self._style_name = None
         self._detyped = None
+        self._d = dict()
+        self._targets = set()
+        if ini_dict:
+            for key, value in ini_dict.items():
+                if value == LsColors.target_value:
+                    self._targets.add(key)
+                    value = LsColors.target_color
+                self._d[key] = value
 
     def __getitem__(self, key):
         return self._d[key]
@@ -346,13 +357,20 @@ def __getitem__(self, key):
     def __setitem__(self, key, value):
         self._detyped = None
         old_value = self._d.get(key, None)
+        self._targets.discard(key)
+        if value == LsColors.target_value:
+            value = LsColors.target_color
+            self._targets.add(key)
         self._d[key] = value
-        if old_value != value:
+        if (
+            old_value != value
+        ):  # bug won't fire if new value is 'target' and old value happened to be no color.
             events.on_lscolors_change.fire(key=key, oldvalue=old_value, newvalue=value)
 
     def __delitem__(self, key):
         self._detyped = None
         old_value = self._d.get(key, None)
+        self._targets.discard(key)
         del self._d[key]
         events.on_lscolors_change.fire(key=key, oldvalue=old_value, newvalue=None)
 
@@ -377,6 +395,10 @@ def _repr_pretty_(self, p, cycle):
                 p.break_()
                 p.pretty(dict(self))
 
+    def is_target(self, key) -> bool:
+        "Return True if key is 'target'"
+        return key in self._targets
+
     def detype(self):
         """De-types the instance, allowing it to be exported to the environment."""
         style = self.style
@@ -387,8 +409,8 @@ def detype(self):
                     + "="
                     + ";".join(
                         [
-                            "target"
-                            if v == "TARGET"
+                            LsColors.target_value
+                            if key in self._targets
                             else ansi_color_name_to_escape_code(v, cmap=style)
                             for v in val
                         ]
@@ -422,28 +444,26 @@ def fromstring(cls, s):
         """Creates a new instance of the LsColors class from a colon-separated
         string of dircolor-valid keys to ANSI color escape sequences.
         """
-        obj = cls()
+        ini_dict = dict()
         # string inputs always use default codes, so translating into
         # xonsh names should be done from defaults
         reversed_default = ansi_reverse_style(style="default")
-        data = {}
         for item in s.split(":"):
             key, eq, esc = item.partition("=")
             if not eq:
                 # not a valid item
                 pass
-            elif esc == "target":
-                data[key] = ("TARGET",)
+            elif esc == LsColors.target_value:  # really only for 'ln'
+                ini_dict[key] = esc
             else:
                 try:
-                    data[key] = ansi_color_escape_code_to_name(
+                    ini_dict[key] = ansi_color_escape_code_to_name(
                         esc, "default", reversed_style=reversed_default
                     )
                 except Exception as e:
                     print("xonsh:warning:" + str(e), file=sys.stderr)
-                    data[key] = ("NO_COLOR",)
-        obj._d = data
-        return obj
+                    ini_dict[key] = ("NO_COLOR",)
+        return cls(ini_dict)
 
     @classmethod
     def fromdircolors(cls, filename=None):
diff --git a/xonsh/lazyimps.py b/xonsh/lazyimps.py
--- a/xonsh/lazyimps.py
+++ b/xonsh/lazyimps.py
@@ -1,4 +1,5 @@
 """Lazy imports that may apply across the xonsh package."""
+import os
 import importlib
 
 from xonsh.platform import ON_WINDOWS, ON_DARWIN
@@ -88,3 +89,11 @@ def terminal256():
 @lazyobject
 def html():
     return importlib.import_module("pygments.formatters.html")
+
+
+@lazyobject
+def os_listxattr():
+    def dummy_listxattr(*args, **kwargs):
+        return []
+
+    return getattr(os, "listxattr", dummy_listxattr)
diff --git a/xonsh/pyghooks.py b/xonsh/pyghooks.py
--- a/xonsh/pyghooks.py
+++ b/xonsh/pyghooks.py
@@ -8,6 +8,7 @@
 from collections import ChainMap
 from collections.abc import MutableMapping
 from keyword import iskeyword
+from xonsh.lazyimps import os_listxattr
 
 from pygments.lexer import inherit, bygroups, include
 from pygments.lexers.agile import Python3Lexer
@@ -1356,15 +1357,16 @@ def on_lscolors_change(key, oldvalue, newvalue, **kwargs):
 events.on_lscolors_change(on_lscolors_change)
 
 
-def color_file(file_path: str, mode: int) -> (Color, str):
-    """Determine color to use for file as ls -c would, given stat() results and its name.
+def color_file(file_path: str, path_stat: os.stat_result) -> (Color, str):
+    """Determine color to use for file *approximately* as ls --color would,
+       given lstat() results and its path.
 
     Parameters
     ----------
-    file_path : string
+    file_path:
         relative path of file (as user typed it).
-    mode : int
-        stat() results for file_path.
+    path_stat:
+        lstat() results for file_path.
 
     Returns
     -------
@@ -1373,54 +1375,80 @@ def color_file(file_path: str, mode: int) -> (Color, str):
     Notes
     -----
 
-    * doesn't handle CA (capability)
-    * doesn't handle LS TARGET mapping
-
+    * implementation follows one authority:
+      https://github.com/coreutils/coreutils/blob/master/src/ls.c#L4879
+    * except:
+    1. does not return 'mi'.  That's the color ls uses to show the (missing) *target* of a symlink
+       (in ls -l, not ls).
+    2. in dircolors, setting type code to '0 or '00' bypasses that test and proceeds to others.
+       In our implementation, setting code to '00' paints the file with no color.
+       This is arguably a bug.
     """
 
     lsc = builtins.__xonsh__.env["LS_COLORS"]
-    color_key = "rs"
+    color_key = "fi"
 
-    if stat.S_ISLNK(mode):  # must test link before S_ISREG (esp execute)
-        color_key = "ln"
+    # if symlink, get info on (final) target
+    if stat.S_ISLNK(path_stat.st_mode):
         try:
-            os.stat(file_path)
-        except FileNotFoundError:
-            color_key = "or"
-    elif stat.S_ISREG(mode):
-        if stat.S_IMODE(mode) & (stat.S_IXUSR + stat.S_IXGRP + stat.S_IXOTH):
-            color_key = "ex"
-        elif (
-            mode & stat.S_ISUID
-        ):  # too many tests before we get to the common case -- restructure?
+            tar_path_stat = os.stat(file_path)  # and work with its properties
+            if lsc.is_target("ln"):  # if ln=target
+                path_stat = tar_path_stat
+        except FileNotFoundError:  # bug always color broken link 'or'
+            color_key = "or"  # early exit
+            ret_color_token = file_color_tokens.get(color_key, Text)
+            return ret_color_token, color_key
+
+    mode = path_stat.st_mode
+
+    if stat.S_ISREG(mode):
+        if mode & stat.S_ISUID:
             color_key = "su"
         elif mode & stat.S_ISGID:
             color_key = "sg"
         else:
-            match = color_file_extension_RE.match(file_path)
-            if match:
-                ext = "*" + match.group(1)  # look for *.<fileExtension> coloring
-                if ext in lsc:
-                    color_key = ext
-                else:
-                    color_key = "rs"
+            cap = os_listxattr(file_path, follow_symlinks=False)
+            if cap and "security.capability" in cap:  # protect None return on some OS?
+                color_key = "ca"
+            elif stat.S_IMODE(mode) & (stat.S_IXUSR + stat.S_IXGRP + stat.S_IXOTH):
+                color_key = "ex"
+            elif path_stat.st_nlink > 1:
+                color_key = "mh"
             else:
-                color_key = "rs"
-    elif stat.S_ISDIR(mode):  # ls -c doesn't colorize sticky or ow if not dirs...
-        color_key = ("di", "ow", "st", "tw")[
-            (mode & stat.S_ISVTX == stat.S_ISVTX) * 2
-            + (mode & stat.S_IWOTH == stat.S_IWOTH)
-        ]
-    elif stat.S_ISCHR(mode):
-        color_key = "cd"
-    elif stat.S_ISBLK(mode):
-        color_key = "bd"
+                color_key = "fi"
+    elif stat.S_ISDIR(mode):  # ls --color doesn't colorize sticky or ow if not dirs...
+        color_key = "di"
+        if not (ON_WINDOWS):  # on Windows, these do not mean what you think they mean.
+            if (mode & stat.S_ISVTX) and (mode & stat.S_IWOTH):
+                color_key = "tw"
+            elif mode & stat.S_IWOTH:
+                color_key = "ow"
+            elif mode & stat.S_ISVTX:
+                color_key = "st"
+    elif stat.S_ISLNK(mode):
+        color_key = "ln"
     elif stat.S_ISFIFO(mode):
         color_key = "pi"
     elif stat.S_ISSOCK(mode):
         color_key = "so"
+    elif stat.S_ISBLK(mode):
+        color_key = "bd"
+    elif stat.S_ISCHR(mode):
+        color_key = "cd"
     elif stat.S_ISDOOR(mode):
-        color_key = "do"  # bug missing mapping for FMT based PORT and WHITEOUT ??
+        color_key = "do"
+    else:
+        color_key = "or"  # any other type --> orphan
+
+    # if still normal file -- try color by file extension.
+    # note: symlink to *.<ext> will be colored 'fi' unless the symlink itself
+    # ends with .<ext>. `ls` does the same.  Bug-for-bug compatibility!
+    if color_key == "fi":
+        match = color_file_extension_RE.match(file_path)
+        if match:
+            ext = "*" + match.group(1)  # look for *.<fileExtension> coloring
+            if ext in lsc:
+                color_key = ext
 
     ret_color_token = file_color_tokens.get(color_key, Text)
 
@@ -1464,8 +1492,8 @@ def subproc_arg_callback(_, match):
     yieldVal = Text
     try:
         path = os.path.expanduser(text)
-        mode = (os.lstat(path)).st_mode  # lstat() will raise FNF if not a real file
-        yieldVal, _ = color_file(path, mode)
+        path_stat = os.lstat(path)  # lstat() will raise FNF if not a real file
+        yieldVal, _ = color_file(path, path_stat)
     except (FileNotFoundError, OSError):
         pass
 


Test patch:
diff --git a/tests/test_environ.py b/tests/test_environ.py
--- a/tests/test_environ.py
+++ b/tests/test_environ.py
@@ -171,6 +171,7 @@ def test_event_on_envvar_change(xonsh_builtins):
     xonsh_builtins.__xonsh__.env = env
     share = []
     # register
+
     @xonsh_builtins.events.on_envvar_change
     def handler(name, oldvalue, newvalue, **kwargs):
         share.extend((name, oldvalue, newvalue))
@@ -186,6 +187,7 @@ def test_event_on_envvar_new(xonsh_builtins):
     xonsh_builtins.__xonsh__.env = env
     share = []
     # register
+
     @xonsh_builtins.events.on_envvar_new
     def handler(name, value, **kwargs):
         share.extend((name, value))
@@ -201,6 +203,7 @@ def test_event_on_envvar_change_from_none_value(xonsh_builtins):
     xonsh_builtins.__xonsh__.env = env
     share = []
     # register
+
     @xonsh_builtins.events.on_envvar_change
     def handler(name, oldvalue, newvalue, **kwargs):
         share.extend((name, oldvalue, newvalue))
@@ -217,6 +220,7 @@ def test_event_on_envvar_change_no_fire_when_value_is_same(val, xonsh_builtins):
     xonsh_builtins.__xonsh__.env = env
     share = []
     # register
+
     @xonsh_builtins.events.on_envvar_change
     def handler(name, oldvalue, newvalue, **kwargs):
         share.extend((name, oldvalue, newvalue))
@@ -232,6 +236,7 @@ def test_events_on_envvar_called_in_right_order(xonsh_builtins):
     xonsh_builtins.__xonsh__.env = env
     share = []
     # register
+
     @xonsh_builtins.events.on_envvar_new
     def handler(name, value, **kwargs):
         share[:] = ["new"]
@@ -303,21 +308,23 @@ def test_delitem_default():
 
 def test_lscolors_target():
     lsc = LsColors.fromstring("ln=target")
-    assert lsc["ln"] == ("TARGET",)
+    assert lsc["ln"] == ("NO_COLOR",)
+    assert lsc.is_target("ln")
     assert lsc.detype() == "ln=target"
+    assert not (lsc.is_target("mi"))
 
 
 @pytest.mark.parametrize(
     "key_in,old_in,new_in,test",
     [
-        ("rs", ("NO_COLOR",), ("BLUE",), "existing key, change value"),
-        ("rs", ("NO_COLOR",), ("NO_COLOR",), "existing key, no change in value"),
+        ("fi", ("NO_COLOR",), ("BLUE",), "existing key, change value"),
+        ("fi", ("NO_COLOR",), ("NO_COLOR",), "existing key, no change in value"),
         ("tw", None, ("NO_COLOR",), "create new key"),
         ("pi", ("BACKGROUND_BLACK", "YELLOW"), None, "delete existing key"),
     ],
 )
 def test_lscolors_events(key_in, old_in, new_in, test, xonsh_builtins):
-    lsc = LsColors.fromstring("rs=0:di=01;34:pi=40;33")
+    lsc = LsColors.fromstring("fi=0:di=01;34:pi=40;33")
     # corresponding colors: [('NO_COLOR',), ('BOLD_CYAN',), ('BOLD_CYAN',), ('BACKGROUND_BLACK', 'YELLOW')]
 
     event_fired = False
diff --git a/tests/test_pyghooks.py b/tests/test_pyghooks.py
--- a/tests/test_pyghooks.py
+++ b/tests/test_pyghooks.py
@@ -2,8 +2,10 @@
 import pytest
 import os
 import stat
+import pathlib
 
 from tempfile import TemporaryDirectory
+from xonsh.platform import ON_WINDOWS
 
 from xonsh.pyghooks import (
     XonshStyle,
@@ -15,7 +17,6 @@
 )
 
 from xonsh.environ import LsColors
-from tools import skip_if_on_windows
 
 
 @pytest.fixture
@@ -25,11 +26,10 @@ def xonsh_builtins_LS_COLORS(xonsh_builtins):
     lsc = LsColors(LsColors.default_settings)
     xonsh_builtins.__xonsh__.env["LS_COLORS"] = lsc
     xonsh_builtins.__xonsh__.shell.shell_type = "prompt_toolkit"
-    # styler = XonshStyle()  # default style
-    # xonsh_builtins.__xonsh__.shell.shell.styler = styler
-    # can't really instantiate XonshStyle separate from a shell??
+    xonsh_builtins.__xonsh__.shell.shell.styler = XonshStyle()  # default style
 
     yield xonsh_builtins
+
     xonsh_builtins.__xonsh__.env = e
 
 
@@ -157,29 +157,62 @@ def test_XonshStyle_init_file_color_tokens(xonsh_builtins_LS_COLORS):
     )
 
 
-_cf = {
-    "rs": "regular",
-    "di": "simple_dir",
-    "ln": "simple_link",
-    "mh": None,
-    "pi": "pipe",
-    "so": None,
-    "do": None,
-    # bug ci failures: 'bd': '/dev/sda',
-    # bug ci failures:'cd': '/dev/tty',
-    "or": "orphan_link",
-    "mi": None,
-    "su": "set_uid",
-    "sg": "set_gid",
-    "ca": None,
-    "tw": "sticky_ow_dir",
-    "ow": "other_writable_dir",
-    "st": "sticky_dir",
-    "ex": "executable",
-    "*.emf": "foo.emf",
-    "*.zip": "foo.zip",
-    "*.ogg": "foo.ogg",
-}
+# parameterized tests for file colorization
+# note 'ca' is checked by standalone test.
+# requires privilege to create a file with capabilities
+
+if ON_WINDOWS:
+    # file coloring support is very limited on Windows, only test the cases we can easily make work
+    # If you care about file colors, use Windows Subsystem for Linux, or another OS.
+
+    _cf = {
+        "fi": "regular",
+        "di": "simple_dir",
+        "ln": "sym_link",
+        "pi": None,
+        "so": None,
+        "do": None,
+        # bug ci failures: 'bd': '/dev/sda',
+        # bug ci failures:'cd': '/dev/tty',
+        "or": "orphan",
+        "mi": None,  # never used
+        "su": None,
+        "sg": None,
+        "ca": None,  # Separate special case test,
+        "tw": None,
+        "ow": None,
+        "st": None,
+        "ex": None,  # executable is a filetype test on Windows.
+        "*.emf": "foo.emf",
+        "*.zip": "foo.zip",
+        "*.ogg": "foo.ogg",
+        "mh": "hard_link",
+    }
+else:
+    # full-fledged, VT100 based infrastructure
+    _cf = {
+        "fi": "regular",
+        "di": "simple_dir",
+        "ln": "sym_link",
+        "pi": "pipe",
+        "so": None,
+        "do": None,
+        # bug ci failures: 'bd': '/dev/sda',
+        # bug ci failures:'cd': '/dev/tty',
+        "or": "orphan",
+        "mi": None,  # never used
+        "su": "set_uid",
+        "sg": "set_gid",
+        "ca": None,  # Separate special case test,
+        "tw": "sticky_ow_dir",
+        "ow": "other_writable_dir",
+        "st": "sticky_dir",
+        "ex": "executable",
+        "*.emf": "foo.emf",
+        "*.zip": "foo.zip",
+        "*.ogg": "foo.ogg",
+        "mh": "hard_link",
+    }
 
 
 @pytest.fixture(scope="module")
@@ -203,18 +236,19 @@ def colorizable_files():
                     os.mkdir(file_path)
                 else:
                     open(file_path, "a").close()
-                if k in ("di", "rg"):
+                if k in ("di", "fi"):
                     pass
                 elif k == "ex":
-                    os.chmod(file_path, stat.S_IXUSR)
-                elif k == "ln":
+                    os.chmod(file_path, stat.S_IRWXU)  # tmpdir on windows need u+w
+                elif k == "ln":  # cook ln test case.
+                    os.chmod(file_path, stat.S_IRWXU)  # link to *executable* file
                     os.rename(file_path, file_path + "_target")
                     os.symlink(file_path + "_target", file_path)
                 elif k == "or":
                     os.rename(file_path, file_path + "_target")
                     os.symlink(file_path + "_target", file_path)
                     os.remove(file_path + "_target")
-                elif k == "pi":
+                elif k == "pi":  # not on Windows
                     os.remove(file_path)
                     os.mkfifo(file_path)
                 elif k == "su":
@@ -232,24 +266,74 @@ def colorizable_files():
                     )
                 elif k == "ow":
                     os.chmod(file_path, stat.S_IWOTH | stat.S_IRUSR | stat.S_IWUSR)
+                elif k == "mh":
+                    os.rename(file_path, file_path + "_target")
+                    os.link(file_path + "_target", file_path)
                 else:
                     pass  # cauterize those elseless ifs!
 
+                os.symlink(file_path, file_path + "_symlink")
+
         yield tempdir
 
     pass  # tempdir get cleaned up here.
 
 
 @pytest.mark.parametrize(
-    "key,file_path", [(key, file_path) for key, file_path in _cf.items() if file_path]
+    "key,file_path", [(key, file_path) for key, file_path in _cf.items() if file_path],
 )
-@skip_if_on_windows
 def test_colorize_file(key, file_path, colorizable_files, xonsh_builtins_LS_COLORS):
-    xonsh_builtins_LS_COLORS.__xonsh__.shell.shell.styler = (
-        XonshStyle()
-    )  # default style
+    """test proper file codes with symlinks colored normally"""
     ffp = colorizable_files + "/" + file_path
-    mode = (os.lstat(ffp)).st_mode
-    color_token, color_key = color_file(ffp, mode)
+    stat_result = os.lstat(ffp)
+    color_token, color_key = color_file(ffp, stat_result)
     assert color_key == key, "File classified as expected kind"
     assert color_token == file_color_tokens[key], "Color token is as expected"
+
+
+@pytest.mark.parametrize(
+    "key,file_path", [(key, file_path) for key, file_path in _cf.items() if file_path],
+)
+def test_colorize_file_symlink(
+    key, file_path, colorizable_files, xonsh_builtins_LS_COLORS
+):
+    """test proper file codes with symlinks colored target."""
+    xonsh_builtins_LS_COLORS.__xonsh__.env["LS_COLORS"]["ln"] = "target"
+    ffp = colorizable_files + "/" + file_path + "_symlink"
+    stat_result = os.lstat(ffp)
+    assert stat.S_ISLNK(stat_result.st_mode)
+
+    _, color_key = color_file(ffp, stat_result)
+
+    try:
+        tar_stat_result = os.stat(ffp)  # stat the target of the link
+        tar_ffp = str(pathlib.Path(ffp).resolve())
+        _, tar_color_key = color_file(tar_ffp, tar_stat_result)
+        if tar_color_key.startswith("*"):
+            tar_color_key = (
+                "fi"  # all the *.* zoo, link is colored 'fi', not target type.
+            )
+    except FileNotFoundError:  # orphan symlinks always colored 'or'
+        tar_color_key = "or"  # Fake if for missing file
+
+    assert color_key == tar_color_key, "File classified as expected kind, via symlink"
+
+
+import xonsh.lazyimps
+
+
+def test_colorize_file_ca(xonsh_builtins_LS_COLORS, monkeypatch):
+    def mock_os_listxattr(*args, **kwards):
+        return ["security.capability"]
+
+    monkeypatch.setattr(xonsh.pyghooks, "os_listxattr", mock_os_listxattr)
+
+    with TemporaryDirectory() as tmpdir:
+        file_path = tmpdir + "/cap_file"
+        open(file_path, "a").close()
+        os.chmod(
+            file_path, stat.S_IRWXU
+        )  # ca overrides ex, leave file deletable on Windows
+        color_token, color_key = color_file(file_path, os.lstat(file_path))
+
+        assert color_key == "ca"