Extract lscolors into its own python project.

This commit is contained in:
Andrew Hamilton 2021-11-06 23:58:17 +10:00
parent 895470b01b
commit a724d234c3
6 changed files with 20 additions and 5 deletions

135
lscolors/lscolors.py Normal file
View file

@ -0,0 +1,135 @@
"""Give coloring for file types as in the ls command."""
import os
import os.path
import stat
import syslog
FILE_KEY = "fi"
DIRECTORY_KEY = "di"
OTHER_WRITABLE_KEY = "ow"
EXECUTABLE_KEY = "ex"
SETUID_KEY = "su"
SETGUID_KEY = "sg"
SYMLINK_KEY = "ln"
ORPHAN_KEY = "or"
PIPE_KEY = "pi"
CHARACTER_DEVICE_KEY = "cd"
BLOCK_DEVICE_KEY = "bd"
STICKY_KEY = "st"
STICKY_OTHER_WRITABLE_KEY = "tw"
SOCKET_KEY = "so"
MISSING_KEY = "mi"
MULTI_HARDLINK_KEY = "mh"
def _parse_ls_colors(ls_codes):
color_codes = {}
for entry in ls_codes.split(":"):
if "=" not in entry:
continue
entry_key, entry_value = entry.split("=")
if entry_key.startswith("*."):
entry_key = entry_key[1:]
color_codes[entry_key] = entry_value
assert color_codes != {}, color_codes
return color_codes
_DEFAULT_COLOR_CODES = \
{BLOCK_DEVICE_KEY: '01;33', SYMLINK_KEY: '01;36',
STICKY_OTHER_WRITABLE_KEY: '30;42', DIRECTORY_KEY: '01;34',
SETUID_KEY: '37;41', CHARACTER_DEVICE_KEY: '01;33', SOCKET_KEY: '01;35',
EXECUTABLE_KEY: '01;32', STICKY_KEY: '37;44',
OTHER_WRITABLE_KEY: '34;42', PIPE_KEY: '33', SETGUID_KEY: '30;43',
ORPHAN_KEY: '40;31;01'}
def get_color_codes(environment):
"""Get a dictionary of the color of every file type."""
if "LS_COLORS" in environment:
try:
return _parse_ls_colors(environment["LS_COLORS"])
except Exception:
syslog.syslog("Syntax error in LS_COLORS environment variable. "
"Using default colors.")
return _DEFAULT_COLOR_CODES
def color_key_for_path(path, color_codes, is_link_target=True):
"""Get the high level type (key) of a file."""
# see print_color_indicator in the file 'ls.c' in the coreutils codebase
if not os.path.lexists(path):
return MISSING_KEY
elif os.path.islink(path):
if is_link_target:
try:
link_path = os.path.join(os.path.dirname(path),
os.readlink(path))
file_stat = os.stat(link_path)
except OSError:
return ORPHAN_KEY
else:
return SYMLINK_KEY
else:
file_stat = os.stat(path)
mode = file_stat.st_mode
if stat.S_ISREG(mode):
if mode & stat.S_ISUID and SETUID_KEY in color_codes:
return SETUID_KEY
elif mode & stat.S_ISGID and SETGUID_KEY in color_codes:
return SETGUID_KEY
elif ((mode & stat.S_IXUSR or mode & stat.S_IXGRP or
mode & stat.S_IXOTH) and EXECUTABLE_KEY in color_codes):
return EXECUTABLE_KEY
elif file_stat.st_nlink > 1 and MULTI_HARDLINK_KEY in color_codes:
return MULTI_HARDLINK_KEY
else:
return FILE_KEY
elif stat.S_ISDIR(mode):
if (mode & stat.S_ISVTX and mode & stat.S_IWOTH and
STICKY_OTHER_WRITABLE_KEY in color_codes):
return STICKY_OTHER_WRITABLE_KEY
elif (mode & stat.S_IWOTH) != 0 and OTHER_WRITABLE_KEY in color_codes:
return OTHER_WRITABLE_KEY
elif (mode & stat.S_ISVTX) != 0 and STICKY_KEY in color_codes:
return STICKY_KEY
else:
return DIRECTORY_KEY
for test_function, color_key in [(stat.S_ISFIFO, PIPE_KEY),
(stat.S_ISSOCK, SOCKET_KEY),
(stat.S_ISBLK, BLOCK_DEVICE_KEY),
(stat.S_ISCHR, CHARACTER_DEVICE_KEY)]:
if test_function(mode):
return color_key
return ORPHAN_KEY
def color_code_for_path(path, color_codes):
"""Get the color of a file."""
def get_extension(basename, color_codes):
parts = basename.split(".")
if len(parts) == 2:
extension = "." + parts[1]
if extension in color_codes:
return extension
elif len(parts) > 2:
for extension in color_codes:
if extension.startswith(".") and \
basename.endswith(extension):
return extension
target_link = color_codes.get(SYMLINK_KEY, None)
color_key = color_key_for_path(path, color_codes,
target_link == "target")
if color_key == FILE_KEY:
filename = os.path.basename(path)
if "." in filename:
extension = get_extension(filename, color_codes)
if extension is not None:
color_key = extension
return color_codes.get(color_key, None)

16
lscolors/setup.py Executable file
View file

@ -0,0 +1,16 @@
#!/usr/bin/env python3.9
try:
from setuptools import setup
except ImportError:
from distutils.core import setup
setup(name="lscolors",
version="2021.10.18",
description=("Give coloring for file types as in the ls command."),
url="https://github.com/ahamilton/eris",
author="Andrew Hamilton",
license="Artistic 2.0",
py_modules=["lscolors"])

270
lscolors/tests/lscolors_test.py Executable file
View file

@ -0,0 +1,270 @@
#!/usr/bin/env python3.9
import os
import os.path
import shutil
import socket
import stat
import subprocess
import tempfile
import unittest
import lscolors
class TempDirTestCase(unittest.TestCase):
def setUp(self):
self.temp_dir = tempfile.mkdtemp()
def tearDown(self):
shutil.rmtree(self.temp_dir)
class ParseLsColorsTestCase(unittest.TestCase):
def test_parse_ls_colors(self):
self.assertRaises(AssertionError, lscolors._parse_ls_colors, "")
self.assertRaises(AssertionError, lscolors._parse_ls_colors, "::")
self.assertEqual(lscolors._parse_ls_colors("*.awk=38;5;148;1"),
{".awk": "38;5;148;1"})
self.assertEqual(lscolors._parse_ls_colors("*.tar.gz=38;5;148;1"),
{".tar.gz": "38;5;148;1"})
self.assertEqual(
lscolors._parse_ls_colors("*.awk=38;5;148;1:di=38;5;30"),
{".awk": "38;5;148;1", "di": "38;5;30"})
class ColorKeyForFileTestCase(TempDirTestCase):
COLOR_CODES = {lscolors.OTHER_WRITABLE_KEY: "other writable",
lscolors.EXECUTABLE_KEY: "executable",
lscolors.ORPHAN_KEY: "orphan",
lscolors.SETGUID_KEY: "setguid",
lscolors.SETUID_KEY: "setuid",
lscolors.STICKY_KEY: "sticky",
lscolors.STICKY_OTHER_WRITABLE_KEY: "sticky other writable",
lscolors.MULTI_HARDLINK_KEY: "multi hardlink",
lscolors.CHARACTER_DEVICE_KEY: "character device",
lscolors.BLOCK_DEVICE_KEY: "block device"}
def test_color_key_for_path_without_extension(self):
executable_path = os.path.join(self.temp_dir, "foo")
open(executable_path, "w").close()
self.assertEqual(
lscolors.color_key_for_path(executable_path, self.COLOR_CODES),
lscolors.FILE_KEY)
def test_color_key_for_path_with_extension(self):
awk_path = os.path.join(self.temp_dir, "test.awk")
open(awk_path, "w").close()
self.assertEqual(
lscolors.color_key_for_path(awk_path, self.COLOR_CODES),
lscolors.FILE_KEY)
def test_color_key_for_path_with_double_extension(self):
tar_gz_path = os.path.join(self.temp_dir, "test.tar.gz")
open(tar_gz_path, "w").close()
self.assertEqual(
lscolors.color_key_for_path(tar_gz_path, self.COLOR_CODES),
lscolors.FILE_KEY)
def test_color_code_for_directory(self):
self.assertEqual(
lscolors.color_key_for_path(self.temp_dir, self.COLOR_CODES),
lscolors.DIRECTORY_KEY)
def test_color_code_for_directory_thats_other_writable(self):
mode = os.stat(self.temp_dir).st_mode
os.chmod(self.temp_dir, mode | stat.S_IWOTH)
self.assertEqual(
lscolors.color_key_for_path(self.temp_dir, self.COLOR_CODES),
lscolors.OTHER_WRITABLE_KEY)
def test_color_code_for_executable(self):
executable_path = os.path.join(self.temp_dir, "a")
open(executable_path, "w").close()
os.chmod(executable_path, stat.S_IEXEC)
self.assertEqual(
lscolors.color_key_for_path(executable_path, self.COLOR_CODES),
lscolors.EXECUTABLE_KEY)
def test_color_code_for_executable_with_extension(self):
executable_path = os.path.join(self.temp_dir, "a.awk")
open(executable_path, "w").close()
os.chmod(executable_path, stat.S_IEXEC)
self.assertEqual(
lscolors.color_key_for_path(executable_path, self.COLOR_CODES),
lscolors.EXECUTABLE_KEY)
def test_color_code_for_setguid(self):
setguid_path = os.path.join(self.temp_dir, "a")
open(setguid_path, "w").close()
os.chmod(setguid_path, stat.S_ISGID)
self.assertEqual(
lscolors.color_key_for_path(setguid_path, self.COLOR_CODES),
lscolors.SETGUID_KEY)
def test_color_code_for_setuid(self):
setuid_path = os.path.join(self.temp_dir, "a")
open(setuid_path, "w").close()
os.chmod(setuid_path, stat.S_ISUID)
self.assertEqual(
lscolors.color_key_for_path(setuid_path, self.COLOR_CODES),
lscolors.SETUID_KEY)
def test_color_code_for_broken_symlink(self):
symlink_path = os.path.join(self.temp_dir, "b")
os.symlink(os.path.join(self.temp_dir, "a"), symlink_path)
self.assertEqual(
lscolors.color_key_for_path(symlink_path, self.COLOR_CODES),
lscolors.ORPHAN_KEY)
def test_color_code_for_good_symlink(self):
symlink_path = os.path.join(self.temp_dir, "b")
awk_path = os.path.join(self.temp_dir, "test.awk")
open(awk_path, "w").close()
os.symlink(awk_path, symlink_path)
self.assertEqual(
lscolors.color_key_for_path(symlink_path, self.COLOR_CODES),
lscolors.FILE_KEY)
def test_color_code_for_pipe(self):
pipe_path = os.path.join(self.temp_dir, "a")
os.mkfifo(pipe_path)
self.assertEqual(
lscolors.color_key_for_path(pipe_path, self.COLOR_CODES),
lscolors.PIPE_KEY)
def test_color_code_for_character_device(self):
character_device_path = "/dev/tty"
self.assertEqual(
lscolors.color_key_for_path(character_device_path,
self.COLOR_CODES),
lscolors.CHARACTER_DEVICE_KEY)
def test_color_code_for_sticky_directory(self):
mode = os.stat(self.temp_dir).st_mode
os.chmod(self.temp_dir, mode | stat.S_ISVTX)
self.assertEqual(
lscolors.color_key_for_path(self.temp_dir, self.COLOR_CODES),
lscolors.STICKY_KEY)
def test_color_code_for_sticky_and_other_writable(self):
mode = os.stat(self.temp_dir).st_mode
os.chmod(self.temp_dir, mode | stat.S_ISVTX | stat.S_IWOTH)
self.assertEqual(
lscolors.color_key_for_path(self.temp_dir, self.COLOR_CODES),
lscolors.STICKY_OTHER_WRITABLE_KEY)
def test_color_code_for_socket(self):
socket_path = os.path.join(self.temp_dir, "socket")
socket_ = socket.socket(socket.AF_UNIX)
socket_.bind(socket_path)
try:
self.assertEqual(
lscolors.color_key_for_path(socket_path, self.COLOR_CODES),
lscolors.SOCKET_KEY)
finally:
socket_.close()
def test_color_code_for_missing_file(self):
missing_path = os.path.join(self.temp_dir, "a")
self.assertEqual(
lscolors.color_key_for_path(missing_path, self.COLOR_CODES),
lscolors.MISSING_KEY)
def test_color_code_for_multi_hardlink(self):
a_path = os.path.join(self.temp_dir, "a")
open(a_path, "w").close()
b_path = os.path.join(self.temp_dir, "b")
os.link(a_path, b_path)
self.assertEqual(
lscolors.color_key_for_path(a_path, self.COLOR_CODES),
lscolors.MULTI_HARDLINK_KEY)
class ColorCodeForFileTestCase(TempDirTestCase):
AWK_COLOR = "awk color"
TAR_GZ_COLOR = "tar gz color"
COLOR_CODES = {
".awk": AWK_COLOR, ".tar.gz": TAR_GZ_COLOR}
def test_color_code_for_path_without_extension(self):
file_path = os.path.join(self.temp_dir, "foo")
open(file_path, "w").close()
self.assertEqual(
lscolors.color_code_for_path(file_path, {"fi": "file color"}),
"file color")
def test_color_code_for_path_with_extension(self):
awk_path = os.path.join(self.temp_dir, "test.awk")
open(awk_path, "w").close()
self.assertEqual(
lscolors.color_code_for_path(awk_path, self.COLOR_CODES),
self.AWK_COLOR)
def test_color_code_for_path_with_double_extension(self):
tar_gz_path = os.path.join(self.temp_dir, "test.tar.gz")
open(tar_gz_path, "w").close()
self.assertEqual(
lscolors.color_code_for_path(tar_gz_path, self.COLOR_CODES),
self.TAR_GZ_COLOR)
def _parse_ls_line(line):
parts = line.split("\x1b[")
if len(parts) == 1:
return (None, line)
for part in parts:
end_color_code = part.find("m")
if end_color_code < (len(part) - 1):
return tuple(part.split("m", 1))
class ParseLsLineTestCase(unittest.TestCase):
def test_parse_ls_line(self):
self.assertEqual(_parse_ls_line(
"\x1b[0m\x1b[38;5;254m\x1b[m\x1b[38;5;30mhello\x1b[0m\n"),
("38;5;30", "hello"))
def test_against_ls(root_path, environment):
process = subprocess.run(
["ls", "--color=always", "-R", root_path],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=environment)
color_codes = lscolors.get_color_codes(environment)
for line in process.stdout.splitlines():
line = line.strip()
if line == "":
continue
if line.endswith(":"):
current_directory = line[:-1]
continue
ls_color_code, filename = _parse_ls_line(line)
path = os.path.join(current_directory, filename)
if os.path.exists(path): # Some paths are already gone. e.g. in /proc
color_code = lscolors.color_code_for_path(path, color_codes)
if color_code != ls_color_code:
print(path, repr(color_code), repr(ls_color_code))
RICH_COLOR_CODES = (
"bd=38;5;68:ca=38;5;17:cd=38;5;113;1:di=38;5;30:do=38;5;127:"
"ex=38;5;166;1:pi=38;5;126:fi=38;5;253:ln=target:mh=38;5;220;1:"
"no=38;5;254:or=48;5;196;38;5;232;1:ow=38;5;33;1:sg=38;5;137;1:"
"su=38;5;137:so=38;5;197:st=48;5;235;38;5;118;1:tw=48;5;235;38;5;139;1:"
"*.BAT=38;5;108:*.PL=38;5;160:*.asm=38;5;240;1:*.awk=38;5;148;1:"
"*.bash=38;5;173:*.bat=38;5;108:*.c=38;5;110:*.cfg=1:*.coffee=38;5;94;1:"
"*.conf=1:*.cpp=38;5;24;1:*.cs=38;5;74;1:*.css=38;5;91:*.csv=38;5;78:"
"*.diff=48;5;197;38;5;232:*.enc=38;5;192;3")
if __name__ == "__main__":
unittest.main()
# root_path = "/"
# test_against_ls(root_path, {"LS_COLORS": RICH_COLOR_CODES})
# test_against_ls(root_path, {}) # Test using default colors