#!/usr/bin/python3 import os import sys import pwd import grp import time import argparse from enum import Enum import subprocess import functools # ANSI escape codes for colors COLORS = { 'RESET': '\033[0m', 'BOLD': '\033[1m', 'DIM': '\033[2m', 'RED': '\033[91m', 'GREEN': '\033[92m', 'YELLOW': '\033[93m', 'BLUE': '\033[94m', 'MAGENTA': '\033[95m', 'CYAN': '\033[96m', 'WHITE': '\033[97m' } class Filetype(Enum): S_IFMT = 0o0170000 S_IFSOCK = 0o140000 S_IFLNK = 0o120000 S_IFREG = 0o100000 S_IFBLK = 0o060000 S_IFDIR = 0o040000 S_IFCHR = 0o020000 S_IFIFO = 0o010000 ERR = 0 def colorize(text, color): return f"{COLORS[color]}{text}{COLORS['RESET']}" def gen_color_file_string(filepath, mode): path, file = os.path.split(filepath) perms = parse_permissions(mode) color = perms['color'] filestring = f"{path and colorize(path+'/', 'DIM')}{colorize(file, color)}" # dircolors var? if perms['char'] == 'l': pointfile = os.readlink(filepath) pointfilereal = os.path.join(os.path.dirname(filepath), os.readlink(filepath)) filestring += ' -> ' + colorize(pointfile, "GREEN" if os.path.exists(pointfilereal) else "RED" ) return filestring def parse_permissions(mode): char = "-" color = "WHITE" prettybits = "" filetype = Filetype(mode & Filetype.S_IFMT.value) or Filetype.ERR match filetype: case Filetype.S_IFSOCK: char = 's' color = "MAGENTA" case Filetype.S_IFLNK: char = 'l' color = "CYAN" case Filetype.S_IFREG: char = '-' if mode & 0o100 or mode & 0o010 or mode & 0o001: # is file executable? color = "GREEN" else: color = "WHITE" case Filetype.S_IFBLK: char = 'b' color = "YELLOW" case Filetype.S_IFDIR: char = 'd' color = "BLUE" case Filetype.S_IFCHR: char = 'c' color = "YELLOW" case Filetype.S_IFIFO: char = 'f' color = "MAGENTA" case _: char = '?' color = "RED" prettybits = char + ''.join([ "r" if mode & 0o400 else "-", "w" if mode & 0o200 else "-", "x" if mode & 0o100 else "-", "r" if mode & 0o040 else "-", "w" if mode & 0o020 else "-", "x" if mode & 0o010 else "-", "r" if mode & 0o004 else "-", "w" if mode & 0o002 else "-", "x" if mode & 0o001 else "-" ]) return {'mode':mode, 'type': filetype, 'char': char, 'color': color, 'prettybits': prettybits } def sizeof_fmt(num): for unit in ("", "K", "M", "G", "T", "P", "E", "Z"): if abs(num) < 1024.0: if unit == "": return f"{num}" #exception to not do .0 for bytes return f"{num:3.1f}{unit}" num /= 1024.0 return f"{num:.1f}Y" def is_path_git_repo_dir(path, mode): #I hate having to take in the mode here, but it makes it easier for usage return Filetype(mode & Filetype.S_IFMT.value) == Filetype.S_IFDIR and os.path.exists(os.path.join(path, '.git')) def git_gud(path): try: output = subprocess.check_output(['git', 'status', '--porcelain'], cwd=path).strip().decode("utf-8") if output.strip(): lines=output.split('\n') mods = len([x for x in lines if x[0] == 'M']) unt = len([x for x in lines if x[0] == '?']) ding = '' ding += colorize('!'+str(mods), "YELLOW") if mods > 0 else '' ding += ' ' if mods > 0 and unt > 0 else '' ding += colorize('?'+str(unt), "CYAN") if unt > 0 else '' return ding # output = subprocess.check_output(['git', 'rev-list', '--count', '--left-only', '@{u}...HEAD'], cwd=file_path, stderr=subprocess.DEVNULL) # if int(output.strip()) > 0: # print("bbbb") except (subprocess.CalledProcessError, FileNotFoundError): pass return '' def list_files(directory='.', args={}): filenames = os.listdir(directory) #get file metadata def get_file_metadata(path, name): rel_file_path = os.path.join(path, name) # for rel paths abs_file_path = os.path.abspath(rel_file_path) # for full path stats = os.lstat(rel_file_path) return { "name": name, "rel_file_path": rel_file_path, "abs_file_path": abs_file_path, "stats": stats } files = [ get_file_metadata(directory, name) for name in os.listdir(directory) ] longest_size_length = len(str(functools.reduce(lambda a, b: a if len(str(a['stats'].st_size)) > len(str(b['stats'].st_size)) else b, files)['stats'].st_size)) print(f"files: {len(files)}") # Sort stuff if args.time_sorted: files.sort(key=lambda f: f["stats"].st_mtime, reverse=(not args.reverse)) elif args.size_sorted: files.sort(key=lambda f: f["stats"].st_size, reverse=(not args.reverse)) else: files.sort(key=lambda f:f["name"], reverse=(args.reverse)) # filter out hidden if not specify -a if not args.all: files = [file for file in files if not file['name'].startswith('.')] if not args.long: for file in files: colored_file = gen_color_file_string(file["abs_file_path"], file["stats"].st_mode) # rel is broken print(colored_file) return for file in files: size = file['stats'].st_size user = pwd.getpwuid(file['stats'].st_uid).pw_name group = grp.getgrgid(file['stats'].st_gid).gr_name permissions = parse_permissions(file['stats'].st_mode) nlinks = file['stats'].st_nlink perm_prettybits = permissions['prettybits'] timestring = time.strftime("%b %d %H:%M", time.localtime(file["stats"].st_mtime)) if args.sparse: if perm_prettybits == 'l': perm_prettybits = 'l ' if permissions['type'] == Filetype.S_IFDIR: size="" if args.human: size=sizeof_fmt(size) if args.full_time: timestring = time.strftime("%b %d %Y %X %z", time.localtime(file["stats"].st_mtime)) # -0400 #mtime = time.strftime("%b %d %X %Z", time.localtime(stats.st_mtime)) # CST colored_file = gen_color_file_string(file["abs_file_path"], file["stats"].st_mode) gitstat = (args.extend and is_path_git_repo_dir(file["abs_file_path"], file["stats"].st_mode) and git_gud(file["abs_file_path"])) or '' ## do printing print(f"{perm_prettybits} {nlinks:2d} {user:<8s} {group:<8s} {size:>{longest_size_length}} {timestring} {colored_file} {gitstat}") if __name__ == "__main__": parser = argparse.ArgumentParser(description="List files in a directory", add_help=False) parser.add_argument('directory', nargs='?', default='.', help="Directory path") # filter options parser.add_argument('-a', '--all', action='store_true', help="Show hidden files") # output information options parser.add_argument('-l', '--long', action='store_true', help="Use a long listing format") parser.add_argument('-F', '--full-time', action='store_true', help="Show full full time") parser.add_argument('-s', '--sparse', action='store_true', help="Blank irrelevant information") # ouput format options sortargs = parser.add_mutually_exclusive_group() sortargs.add_argument('-t', '--time-sorted', action='store_true', help="Sort by time") sortargs.add_argument('-S', '--size-sorted', action='store_true', help="Sort by file size") parser.add_argument('-r', '--reverse', action='store_true', help="Reverse sort order") parser.add_argument('-h', '--human', action='store_true', help="Human Readable size") # extra parser.add_argument('-e', '--extend', action='store_true', help="Do some extra processing on the directory") args = parser.parse_args() print(f"ls.py {' '.join(sys.argv[1:])}") list_files(args.directory, args)