Skip to content

Instantly share code, notes, and snippets.

@jsutlovic
Created September 14, 2023 15:45
Show Gist options
  • Save jsutlovic/2bd00af19466c8cd4aeaff32bca1daf9 to your computer and use it in GitHub Desktop.
Save jsutlovic/2bd00af19466c8cd4aeaff32bca1daf9 to your computer and use it in GitHub Desktop.
Show lscolors on macOS
#!/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