dircolors.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. #!/usr/bin/env python
  2. # License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
  3. import os
  4. import stat
  5. from contextlib import suppress
  6. from typing import Dict, Generator, Optional, Tuple, Union
  7. DEFAULT_DIRCOLORS = r"""# {{{
  8. # Configuration file for dircolors, a utility to help you set the
  9. # LS_COLORS environment variable used by GNU ls with the --color option.
  10. # Copyright (C) 1996-2019 Free Software Foundation, Inc.
  11. # Copying and distribution of this file, with or without modification,
  12. # are permitted provided the copyright notice and this notice are preserved.
  13. # The keywords COLOR, OPTIONS, and EIGHTBIT (honored by the
  14. # slackware version of dircolors) are recognized but ignored.
  15. # Below are TERM entries, which can be a glob patterns, to match
  16. # against the TERM environment variable to determine if it is colorizable.
  17. TERM Eterm
  18. TERM ansi
  19. TERM *color*
  20. TERM con[0-9]*x[0-9]*
  21. TERM cons25
  22. TERM console
  23. TERM cygwin
  24. TERM dtterm
  25. TERM gnome
  26. TERM hurd
  27. TERM jfbterm
  28. TERM konsole
  29. TERM kterm
  30. TERM linux
  31. TERM linux-c
  32. TERM mlterm
  33. TERM putty
  34. TERM rxvt*
  35. TERM screen*
  36. TERM st
  37. TERM terminator
  38. TERM tmux*
  39. TERM vt100
  40. TERM xterm*
  41. # Below are the color init strings for the basic file types.
  42. # One can use codes for 256 or more colors supported by modern terminals.
  43. # The default color codes use the capabilities of an 8 color terminal
  44. # with some additional attributes as per the following codes:
  45. # Attribute codes:
  46. # 00=none 01=bold 04=underscore 05=blink 07=reverse 08=concealed
  47. # Text color codes:
  48. # 30=black 31=red 32=green 33=yellow 34=blue 35=magenta 36=cyan 37=white
  49. # Background color codes:
  50. # 40=black 41=red 42=green 43=yellow 44=blue 45=magenta 46=cyan 47=white
  51. #NORMAL 00 # no color code at all
  52. #FILE 00 # regular file: use no color at all
  53. RESET 0 # reset to "normal" color
  54. DIR 01;34 # directory
  55. LINK 01;36 # symbolic link. (If you set this to 'target' instead of a
  56. # numerical value, the color is as for the file pointed to.)
  57. MULTIHARDLINK 00 # regular file with more than one link
  58. FIFO 40;33 # pipe
  59. SOCK 01;35 # socket
  60. DOOR 01;35 # door
  61. BLK 40;33;01 # block device driver
  62. CHR 40;33;01 # character device driver
  63. ORPHAN 40;31;01 # symlink to nonexistent file, or non-stat'able file ...
  64. MISSING 00 # ... and the files they point to
  65. SETUID 37;41 # file that is setuid (u+s)
  66. SETGID 30;43 # file that is setgid (g+s)
  67. CAPABILITY 30;41 # file with capability
  68. STICKY_OTHER_WRITABLE 30;42 # dir that is sticky and other-writable (+t,o+w)
  69. OTHER_WRITABLE 34;42 # dir that is other-writable (o+w) and not sticky
  70. STICKY 37;44 # dir with the sticky bit set (+t) and not other-writable
  71. # This is for files with execute permission:
  72. EXEC 01;32
  73. # List any file extensions like '.gz' or '.tar' that you would like ls
  74. # to colorize below. Put the extension, a space, and the color init string.
  75. # (and any comments you want to add after a '#')
  76. # If you use DOS-style suffixes, you may want to uncomment the following:
  77. #.cmd 01;32 # executables (bright green)
  78. #.exe 01;32
  79. #.com 01;32
  80. #.btm 01;32
  81. #.bat 01;32
  82. # Or if you want to colorize scripts even if they do not have the
  83. # executable bit actually set.
  84. #.sh 01;32
  85. #.csh 01;32
  86. # archives or compressed (bright red)
  87. .tar 01;31
  88. .tgz 01;31
  89. .arc 01;31
  90. .arj 01;31
  91. .taz 01;31
  92. .lha 01;31
  93. .lz4 01;31
  94. .lzh 01;31
  95. .lzma 01;31
  96. .tlz 01;31
  97. .txz 01;31
  98. .tzo 01;31
  99. .t7z 01;31
  100. .zip 01;31
  101. .z 01;31
  102. .dz 01;31
  103. .gz 01;31
  104. .lrz 01;31
  105. .lz 01;31
  106. .lzo 01;31
  107. .xz 01;31
  108. .zst 01;31
  109. .tzst 01;31
  110. .bz2 01;31
  111. .bz 01;31
  112. .tbz 01;31
  113. .tbz2 01;31
  114. .tz 01;31
  115. .deb 01;31
  116. .rpm 01;31
  117. .jar 01;31
  118. .war 01;31
  119. .ear 01;31
  120. .sar 01;31
  121. .rar 01;31
  122. .alz 01;31
  123. .ace 01;31
  124. .zoo 01;31
  125. .cpio 01;31
  126. .7z 01;31
  127. .rz 01;31
  128. .cab 01;31
  129. .wim 01;31
  130. .swm 01;31
  131. .dwm 01;31
  132. .esd 01;31
  133. # image formats
  134. .jpg 01;35
  135. .jpeg 01;35
  136. .mjpg 01;35
  137. .mjpeg 01;35
  138. .gif 01;35
  139. .bmp 01;35
  140. .pbm 01;35
  141. .pgm 01;35
  142. .ppm 01;35
  143. .tga 01;35
  144. .xbm 01;35
  145. .xpm 01;35
  146. .tif 01;35
  147. .tiff 01;35
  148. .png 01;35
  149. .svg 01;35
  150. .svgz 01;35
  151. .mng 01;35
  152. .pcx 01;35
  153. .mov 01;35
  154. .mpg 01;35
  155. .mpeg 01;35
  156. .m2v 01;35
  157. .mkv 01;35
  158. .webm 01;35
  159. .ogm 01;35
  160. .mp4 01;35
  161. .m4v 01;35
  162. .mp4v 01;35
  163. .vob 01;35
  164. .qt 01;35
  165. .nuv 01;35
  166. .wmv 01;35
  167. .asf 01;35
  168. .rm 01;35
  169. .rmvb 01;35
  170. .flc 01;35
  171. .avi 01;35
  172. .fli 01;35
  173. .flv 01;35
  174. .gl 01;35
  175. .dl 01;35
  176. .xcf 01;35
  177. .xwd 01;35
  178. .yuv 01;35
  179. .cgm 01;35
  180. .emf 01;35
  181. # https://wiki.xiph.org/MIME_Types_and_File_Extensions
  182. .ogv 01;35
  183. .ogx 01;35
  184. # audio formats
  185. .aac 00;36
  186. .au 00;36
  187. .flac 00;36
  188. .m4a 00;36
  189. .mid 00;36
  190. .midi 00;36
  191. .mka 00;36
  192. .mp3 00;36
  193. .mpc 00;36
  194. .ogg 00;36
  195. .ra 00;36
  196. .wav 00;36
  197. # https://wiki.xiph.org/MIME_Types_and_File_Extensions
  198. .oga 00;36
  199. .opus 00;36
  200. .spx 00;36
  201. .xspf 00;36
  202. """ # }}}
  203. # special file?
  204. special_types = (
  205. (stat.S_IFLNK, 'ln'), # symlink
  206. (stat.S_IFIFO, 'pi'), # pipe (FIFO)
  207. (stat.S_IFSOCK, 'so'), # socket
  208. (stat.S_IFBLK, 'bd'), # block device
  209. (stat.S_IFCHR, 'cd'), # character device
  210. (stat.S_ISUID, 'su'), # setuid
  211. (stat.S_ISGID, 'sg'), # setgid
  212. )
  213. CODE_MAP = {
  214. 'RESET': 'rs',
  215. 'DIR': 'di',
  216. 'LINK': 'ln',
  217. 'MULTIHARDLINK': 'mh',
  218. 'FIFO': 'pi',
  219. 'SOCK': 'so',
  220. 'DOOR': 'do',
  221. 'BLK': 'bd',
  222. 'CHR': 'cd',
  223. 'ORPHAN': 'or',
  224. 'MISSING': 'mi',
  225. 'SETUID': 'su',
  226. 'SETGID': 'sg',
  227. 'CAPABILITY': 'ca',
  228. 'STICKY_OTHER_WRITABLE': 'tw',
  229. 'OTHER_WRITABLE': 'ow',
  230. 'STICKY': 'st',
  231. 'EXEC': 'ex',
  232. }
  233. def stat_at(file: str, cwd: Optional[Union[int, str]] = None, follow_symlinks: bool = False) -> os.stat_result:
  234. dirfd: Optional[int] = None
  235. need_to_close = False
  236. if isinstance(cwd, str):
  237. dirfd = os.open(cwd, os.O_RDONLY | getattr(os, 'O_CLOEXEC', 0))
  238. need_to_close = True
  239. elif isinstance(cwd, int):
  240. dirfd = cwd
  241. try:
  242. return os.stat(file, dir_fd=dirfd, follow_symlinks=follow_symlinks)
  243. finally:
  244. if need_to_close and dirfd is not None:
  245. os.close(dirfd)
  246. class Dircolors:
  247. def __init__(self) -> None:
  248. self.codes: Dict[str, str] = {}
  249. self.extensions: Dict[str, str] = {}
  250. if not self.load_from_environ() and not self.load_from_file():
  251. self.load_defaults()
  252. def clear(self) -> None:
  253. self.codes.clear()
  254. self.extensions.clear()
  255. def load_from_file(self) -> bool:
  256. for candidate in (os.path.expanduser('~/.dir_colors'), '/etc/DIR_COLORS'):
  257. with suppress(Exception):
  258. with open(candidate) as f:
  259. return self.load_from_dircolors(f.read())
  260. return False
  261. def load_from_lscolors(self, lscolors: str) -> bool:
  262. self.clear()
  263. if not lscolors:
  264. return False
  265. for item in lscolors.split(':'):
  266. try:
  267. code, color = item.split('=', 1)
  268. except ValueError:
  269. continue
  270. if code.startswith('*.'):
  271. self.extensions[code[1:]] = color
  272. else:
  273. self.codes[code] = color
  274. return bool(self.codes or self.extensions)
  275. def load_from_environ(self, envvar: str = 'LS_COLORS') -> bool:
  276. return self.load_from_lscolors(os.environ.get(envvar) or '')
  277. def load_from_dircolors(self, database: str, strict: bool = False) -> bool:
  278. self.clear()
  279. for line in database.splitlines():
  280. line = line.split('#')[0].strip()
  281. if not line:
  282. continue
  283. split = line.split()
  284. if len(split) != 2:
  285. if strict:
  286. raise ValueError(f'Warning: unable to parse dircolors line "{line}"')
  287. continue
  288. key, val = split
  289. if key == 'TERM':
  290. continue
  291. if key in CODE_MAP:
  292. self.codes[CODE_MAP[key]] = val
  293. elif key.startswith('.'):
  294. self.extensions[key] = val
  295. elif strict:
  296. raise ValueError(f'Warning: unable to parse dircolors line "{line}"')
  297. return bool(self.codes or self.extensions)
  298. def load_defaults(self) -> bool:
  299. self.clear()
  300. return self.load_from_dircolors(DEFAULT_DIRCOLORS, True)
  301. def generate_lscolors(self) -> str:
  302. """ Output the database in the format used by the LS_COLORS environment variable. """
  303. def gen_pairs() -> Generator[Tuple[str, str], None, None]:
  304. for pair in self.codes.items():
  305. yield pair
  306. for pair in self.extensions.items():
  307. # change .xyz to *.xyz
  308. yield '*' + pair[0], pair[1]
  309. return ':'.join('{}={}'.format(*pair) for pair in gen_pairs())
  310. def _format_code(self, text: str, code: str) -> str:
  311. val = self.codes.get(code)
  312. return '\033[{}m{}\033[{}m'.format(val, text, self.codes.get('rs', '0')) if val else text
  313. def _format_ext(self, text: str, ext: str) -> str:
  314. val = self.extensions.get(ext, '0')
  315. return '\033[{}m{}\033[{}m'.format(val, text, self.codes.get('rs', '0')) if val else text
  316. def format_mode(self, text: str, sr: os.stat_result) -> str:
  317. mode = sr.st_mode
  318. if stat.S_ISDIR(mode):
  319. if (mode & (stat.S_ISVTX | stat.S_IWOTH)) == (stat.S_ISVTX | stat.S_IWOTH):
  320. # sticky and world-writable
  321. return self._format_code(text, 'tw')
  322. if mode & stat.S_ISVTX:
  323. # sticky but not world-writable
  324. return self._format_code(text, 'st')
  325. if mode & stat.S_IWOTH:
  326. # world-writable but not sticky
  327. return self._format_code(text, 'ow')
  328. # normal directory
  329. return self._format_code(text, 'di')
  330. for mask, code in special_types:
  331. if (mode & mask) == mask:
  332. return self._format_code(text, code)
  333. # executable file?
  334. if mode & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH):
  335. return self._format_code(text, 'ex')
  336. # regular file, format according to its extension
  337. ext = os.path.splitext(text)[1]
  338. if ext:
  339. return self._format_ext(text, ext)
  340. return text
  341. def __call__(self, path: str, text: str, cwd: Optional[Union[int, str]] = None) -> str:
  342. follow_symlinks = self.codes.get('ln') == 'target'
  343. try:
  344. sr = stat_at(path, cwd, follow_symlinks)
  345. except OSError:
  346. return text
  347. return self.format_mode(text, sr)
  348. def develop() -> None:
  349. import sys
  350. print(Dircolors()(sys.argv[-1], sys.argv[-1]))