297 lines
10 KiB
Python
Executable File
297 lines
10 KiB
Python
Executable File
#!/usr/bin/python3
|
|
import os
|
|
import sys
|
|
import pwd
|
|
import grp
|
|
import time
|
|
import argparse
|
|
from enum import Enum
|
|
import subprocess
|
|
import functools
|
|
from collections import namedtuple
|
|
|
|
# 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
|
|
|
|
FileMeta = namedtuple("FileMeta", "name rel_file_path abs_file_path stats")
|
|
|
|
Permissions = namedtuple("Permissions", "mode type char color prettybits")
|
|
|
|
FileString = namedtuple("FileString", ["path", "file", "file_color",
|
|
"path_color", "colored_path",
|
|
"colored_file", "colored_filepath",
|
|
"link_sep", "link_dest",
|
|
"link_dest_full", "link_color",
|
|
"colored_link_dest"
|
|
], defaults=['','','','',''])
|
|
|
|
def colorwrap(text, color):
|
|
return f"{COLORS[color]}{text}{COLORS['RESET']}"
|
|
|
|
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 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 }
|
|
return Permissions(mode, filetype, char, color, prettybits)
|
|
|
|
|
|
|
|
def file_string_gen(filepath, mode):
|
|
path, file = os.path.split(filepath)
|
|
perms = parse_permissions(mode)
|
|
file_color = perms.color
|
|
path_color = 'DIM'
|
|
|
|
colored_path = colorwrap(path+'/', path_color) if path else ''
|
|
colored_file = colorwrap(file, file_color)
|
|
colored_filepath = colored_path + colored_file
|
|
|
|
# links
|
|
if perms.type == Filetype.S_IFLNK:
|
|
link_sep = ' -> '
|
|
link_dest = os.readlink(filepath)
|
|
link_dest_real = os.path.join(os.path.dirname(filepath), os.readlink(filepath))
|
|
link_color = "GREEN" if os.path.exists(link_dest_real) else "RED"
|
|
colored_link_dest = colorwrap(link_dest, link_color)
|
|
return FileString(path, file, file_color, path_color, colored_path, colored_file, colored_filepath, link_sep, link_dest, link_dest_real, link_color, colored_link_dest)
|
|
|
|
return FileString(path, file, file_color, path_color, colored_path, colored_file, colored_filepath)
|
|
|
|
|
|
|
|
|
|
|
|
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 += colorwrap('!'+str(mods), "YELLOW") if mods > 0 else ''
|
|
ding += ' ' if mods > 0 and unt > 0 else ''
|
|
ding += colorwrap('?'+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 short_listing(files_meta):
|
|
for file in files_meta:
|
|
colored_file = file_string_gen(file.name, file.stats.st_mode).colored_filepath
|
|
print(colored_file)
|
|
#colored_file = gen_color_file_string(file.abs_file_path, file.stats.st_mode)
|
|
# rel is broken
|
|
#print(colored_file)
|
|
|
|
def long_listing(files_meta):
|
|
use_full_path = True
|
|
|
|
print(f"ls.py {' '.join(sys.argv[1:])}")
|
|
print(f"files: {len(files_meta)}")
|
|
|
|
term_width = os.get_terminal_size()
|
|
|
|
|
|
longest_size_string_len = len(str(functools.reduce(lambda a, b: a if len(str(a.stats.st_size)) > len(str(b.stats.st_size)) else b, files_meta).stats.st_size))
|
|
|
|
|
|
# if use_full_path:
|
|
# longest_filename_string_len = len(functools.reduce(lambda a, b: a if len(a.abs_file_path) > len(b.abs_file_path) else b, files_meta).abs_file_path)
|
|
# else:
|
|
# longest_filename_string_len = len(functools.reduce(lambda a, b: a if len(a.rel_file_path) > len(b.rel_file_path) else b, files_meta).rel_file_path)
|
|
#
|
|
|
|
|
|
for file in files_meta:
|
|
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
|
|
|
|
file_strings = file_string_gen(file.abs_file_path, file.stats.st_mode)
|
|
file_str = ' ' + file_strings.colored_filepath + file_strings.link_sep + file_strings.colored_link_dest
|
|
#so hacky
|
|
if ' ' in file.abs_file_path:
|
|
file_str = '\'' + file_strings.colored_filepath + '\' ' + file_strings.link_sep + file_strings.colored_link_dest
|
|
|
|
#print(file_string_gen(file["abs_file_path"], file["stats"].st_mode))
|
|
gitstat = git_gud(file.abs_file_path) if (args.extend and is_path_git_repo_dir(file.abs_file_path, file.stats.st_mode)) else ''
|
|
|
|
## do printing
|
|
print(f"{perm_prettybits} {nlinks:2d} {user:<8s} {group:<8s} {size:>{longest_size_string_len}} {timestring} {file_str} {gitstat}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def list_files(directory='.', args={}):
|
|
|
|
dir_filenames = os.listdir(directory)
|
|
|
|
#get file metadata
|
|
files = []
|
|
|
|
# since os.listdir does not handle . or .. dirs
|
|
files += [FileMeta('.', directory + '/.', os.path.abspath(directory )+'/.', os.lstat(directory +'/.'))]
|
|
files += [FileMeta('..', directory + '/..', os.path.abspath(directory )+'/..', os.lstat(directory +'/..'))]
|
|
|
|
for name in dir_filenames:
|
|
rel_file_path = os.path.join(directory, name) # for rel paths
|
|
abs_file_path = os.path.abspath(rel_file_path) # for full path
|
|
stats = os.lstat(rel_file_path)
|
|
files.append(FileMeta(name, rel_file_path, abs_file_path, stats))
|
|
|
|
|
|
# filter out options
|
|
if not args.all:
|
|
files = [file for file in files if not file.name.startswith('.')]
|
|
|
|
# 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))
|
|
|
|
# normal listing basically
|
|
if not args.long:
|
|
short_listing(files)
|
|
else:
|
|
long_listing(files)
|
|
|
|
|
|
|
|
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()
|
|
|
|
list_files(args.directory, args)
|