Created
September 14, 2023 15:45
-
-
Save jsutlovic/2bd00af19466c8cd4aeaff32bca1daf9 to your computer and use it in GitHub Desktop.
Show lscolors on macOS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# Script to parse a line of LSCOLORS and outputs examples of parsed line to stdout | |
# | |
# Run tests with python3 -m doctest lscolors.py | |
# From `man ls`: | |
# | |
# The color designators are as follows: | |
# a black | |
# b red | |
# c green | |
# d brown | |
# e blue | |
# f magenta | |
# g cyan | |
# h light grey | |
# A bold black, usually shows up as dark grey | |
# B bold red | |
# C bold green | |
# D bold brown, usually shows up as yellow | |
# E bold blue | |
# F bold magenta | |
# G bold cyan | |
# H bold light grey; looks like bright white | |
# x default foreground or background | |
# | |
# The order of the attributes are as follows: | |
# | |
# 1. directory | |
# 2. symbolic link | |
# 3. socket | |
# 4. pipe | |
# 5. executable | |
# 6. block special | |
# 7. character special | |
# 8. executable with setuid bit set | |
# 9. executable with setgid bit set | |
# 10. directory writable to others, with sticky bit | |
# 11. directory writable to others, without sticky bit | |
import os | |
import sys | |
import re | |
from enum import Enum | |
from textwrap import dedent | |
from typing import NamedTuple | |
test_lines: list[ | |
str | |
] = """\ | |
1. {}directory ({}) | |
2. {}symbolic link ({}) | |
3. {}socket ({}) | |
4. {}pipe ({}) | |
5. {}executable ({}) | |
6. {}block special ({}) | |
7. {}character special ({}) | |
8. {}executable with setuid bit set ({}) | |
9. {}executable with setgid bit set ({}) | |
10. {}directory writable to others, with sticky bit ({}) | |
11. {}directory writable to others, without sticky bit ({}) | |
""".splitlines() | |
def usage(name: str): | |
usage = dedent( | |
"""\ | |
{name} [colorstring] | |
{bold}colorstring{reset} is a string of 22 characters, 11 pairs of 2-character combinations | |
denoting foreground and background text coloring that should be applied to | |
a given filetype. | |
Example: Gxfxcxdxbxegedabagacad | |
If colorstring is not provided, the script will pull it from the LSCOLORS | |
environment variable. The script then parses the given LSCOLORS string and | |
outputs a sample of each file type along with its coloring. | |
See {bold}man ls{reset} on macOS for LSCOLORS format. | |
""" | |
).format(name=name, bold="\033[1;39m", reset="\033[0m") | |
print(usage) | |
sys.exit(1) | |
class ColorNum(Enum): | |
BLACK = 0 | |
RED = 1 | |
GREEN = 2 | |
YELLOW = 3 | |
BLUE = 4 | |
MAGENTA = 5 | |
CYAN = 6 | |
LIGHT_GREY = 7 | |
DEFAULT = 9 | |
class ColorIndex(NamedTuple): | |
bold: bool | |
color: ColorNum | |
foreground: bool = True | |
reset: bool = False | |
@staticmethod | |
def from_str(char: str, foreground: bool = True) -> "ColorIndex": | |
"""Return a ColorIndex value based on a string character | |
ColorIndex from string "E" should be blue and bold | |
>>> ColorIndex.from_str("E") | |
ColorIndex(bold=True, color=<ColorNum.BLUE: 4>, foreground=True, reset=False) | |
ColorIndex from string "b" and not foreground should be red and not bold | |
>>> ColorIndex.from_str("b", foreground=False) | |
ColorIndex(bold=False, color=<ColorNum.RED: 1>, foreground=False, reset=False) | |
ColorIndex with default value | |
>>> ColorIndex.from_str("x") | |
ColorIndex(bold=False, color=<ColorNum.DEFAULT: 9>, foreground=True, reset=False) | |
""" | |
# Do some ASCII bit manipulation | |
c = ord(char) | |
bold = not (c & 0x20) | |
if char == "x": | |
colornum = ColorNum.DEFAULT.value | |
else: | |
colornum = (c - ord("A")) & 0x1F | |
return ColorIndex(bold, ColorNum(colornum), foreground) | |
def to_ansi(self) -> str: | |
"""Return an ANSI color escape representing the ColorIndex value | |
>>> ColorIndex(bold=True, color=ColorNum.RED, foreground=True, reset=False).to_ansi() | |
'\\x1b[1;31m' | |
>>> ColorIndex(bold=False, color=ColorNum.GREEN, foreground=False, reset=False).to_ansi() | |
'\\x1b[42m' | |
>>> ColorIndex(bold=False, color=ColorNum(0), foreground=False, reset=True).to_ansi() | |
'\\x1b[0m' | |
>>> ColorIndex(bold=True, color=ColorNum.DEFAULT, foreground=True, reset=False).to_ansi() | |
'\\x1b[39m' | |
>>> ColorIndex(bold=False, color=ColorNum.DEFAULT, foreground=False, reset=False).to_ansi() | |
'\\x1b[49m' | |
""" | |
ansi_str = "\033[" | |
if self.reset: | |
ansi_str += "0m" | |
return ansi_str | |
if self.bold and self.foreground and self.color != ColorNum.DEFAULT: | |
ansi_str += "1;" | |
base = 30 | |
if not self.foreground: | |
base = 40 | |
ansi_str += "{0}m".format(base + self.color.value) | |
return ansi_str | |
class ColorPair(NamedTuple): | |
foreground: ColorIndex | |
background: ColorIndex | |
pair: str | |
def to_ansi(self) -> str: | |
"""Color terminal output based on the color pair given | |
>>> ColorPair.from_str("Ex")[0].to_ansi() | |
'\\x1b[1;34m\\x1b[49m' | |
>>> ColorPair.from_str("ab")[0].to_ansi() | |
'\\x1b[30m\\x1b[41m' | |
""" | |
return self.foreground.to_ansi() + self.background.to_ansi() | |
@staticmethod | |
def from_str(colorstring: str) -> "list[ColorPair]": | |
"""Parse an LSCOLOR string into ColorPairs of foreground and background | |
ColorIndex values | |
>>> ColorPair.from_str("Exab") # doctest: +NORMALIZE_WHITESPACE | |
[ColorPair(foreground=ColorIndex(bold=True, color=<ColorNum.BLUE: 4>, foreground=True, reset=False), | |
background=ColorIndex(bold=False, color=<ColorNum.DEFAULT: 9>, foreground=False, reset=False), | |
pair='Ex'), | |
ColorPair(foreground=ColorIndex(bold=False, color=<ColorNum.BLACK: 0>, foreground=True, reset=False), | |
background=ColorIndex(bold=False, color=<ColorNum.RED: 1>, foreground=False, reset=False), | |
pair='ab')] | |
""" | |
return [ | |
ColorPair( | |
ColorIndex.from_str(colorstring[i]), | |
ColorIndex.from_str(colorstring[i + 1], foreground=False), | |
colorstring[i : i + 2], | |
) | |
for i in range(0, len(colorstring), 2) | |
] | |
def validate_colorstring(colorstring: str) -> bool: | |
return re.match(r"^[a-hA-Hx]{22}$", colorstring) != None | |
def map_colors(colorstring: str) -> None: | |
colorpairs = ColorPair.from_str(colorstring) | |
reset = ColorIndex(False, ColorNum.BLACK, False, True) | |
for color, line in zip(colorpairs, test_lines): | |
colored_line = line.format(color.to_ansi(), color.pair) + reset.to_ansi() | |
print(colored_line) | |
def main() -> None: | |
name = os.path.basename(sys.argv[0]) | |
args = sys.argv[1:] | |
if len(args) > 1: | |
usage(name) | |
colorstring = "" | |
if len(args) == 0: | |
colorstring = os.environ.get("LSCOLORS", "") | |
else: | |
colorstring = args[0] | |
if colorstring == "-h": | |
usage(name) | |
if not validate_colorstring(colorstring): | |
sys.stderr.write("Invalid colorstring format: {}\n\n".format(repr(colorstring))) | |
usage(name) | |
print('LSCOLORS="{}"\n'.format(colorstring)) | |
# process colorstring | |
map_colors(colorstring) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment