#!/usr/bin/env python # cls = (categorical|color)ls. Some useful utility functions, too (e.g. table) # XXX Should files specifically listed on the command line be filter()ed out? # XXX st_rdev, st_blocks will cause exceptions if used on Python < 2.2. Hmm. # TODO OS-specific code to decompose device numbers into (major, minor). # TODO varying per-type sub-orders: File-instance-level __cmp__ methods? import os, re, pwd, grp, string, sys from posixpath import basename,splitext,dirname from time import strftime, localtime from stat import * # 'stat' names prefixed for import * env = os.environ use_color = 1 ## deflt to using color cmp_sign = [ ] ordbrok = 0 fmtbrok = 0 def ioctl_GWINSZ(fd): #### TABULATION FUNCTIONS try: ### Discover terminal width import fcntl, termios, struct, os cr = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234')) except: return None return cr def terminal_size(): ### decide on *some* terminal size cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2) # try open fds if not cr: # ...then ctty try: fd = os.open(os.ctermid(), os.O_RDONLY) cr = ioctl_GWINSZ(fd) os.close(fd) except: pass if not cr: # env vars or finally defaults try: cr = (env['LINES'], env['COLUMNS']) except: cr = (25, 80) return int(cr[1]), int(cr[0]) # reverse rows, cols def rowscols(n, nc): ### handle ceil(n/nc) assignment div, mod = divmod(n, nc) return div + (mod != 0), nc def sum(x): return reduce(lambda x, y: x + y, x) def layout(strs, lens, W, gap, mx, pd): ### Determine layout n = len(strs) nrow, ncol = rowscols(n, 1) # 1->W/(max(lens)+gap) ws = [ W / ncol ] * ncol while nrow > 1 and ncol < mx: nrow0 = nrow # add col until at least while nrow >= nrow0: # one row can be elided nrow, ncol = rowscols(n, ncol + 1) ws_try = [ ] # accumulate widths for c in range(ncol): a, b = nrow * c , min(nrow * (c+1), n) w1 = max(lens[a : b]) + gap # width = widest + gap ws_try.append(w1) ws_try[-1] = ws_try[-1] - gap if sum(ws_try) >= W: # out of room. DONE break ws = ws_try # new cols fit nrow, ncol = rowscols(n, len(ws)) excess = W - sum(ws) # excess space -> gaps c = 0 added = 0 while excess > 0 and added < ncol * pd: added += 1 # distribute at most pd spc per col ws[c] = ws[c] + 1 excess = excess - 1 c = (c + 1) % (ncol - 1) # cyclic index return nrow, ncol, ws def table(strs, w=terminal_size()[0], ### Format any string list as table pfx='', gap=2, mx=99, L=len, pd=8): lens = map(L, strs) nrow, ncol, ws = layout(strs, lens, w, gap, mx, pd) out = '' for r in range(nrow): ## build output out = out + pfx for c in range(ncol): i = nrow * c + r if i >= len(strs): break out = out + strs[i] + ((ws[c] - lens[i]) * ' ') out = out + '\n' return out attrs = { #### COLORIZATION (CAPS -> background) 'bold' : '01', 'italic' : '04', 'blink' : '05', 'inverse': '07', 'black' : '30', 'red' : '31', 'green' : '32', 'yellow' : '33', 'blue' : '34', 'purple' : '35', 'cyan' : '36', 'white' : '37', 'BLACK' : '40', 'RED' : '41', 'GREEN' : '42', 'YELLOW' : '43', 'BLUE' : '44', 'CYAN' : '45', 'PURPLE' : '46', 'WHITE' : '47' } attrOFF = '\x1b[00m' def mkcolor(specifier): ### \e[$A;3$F;4$Bm for attr A,colr F,B if not len(specifier): return '', '' ret = '\x1b[' for word in string.split(specifier): try: ret = ret + attrs[word] + ';' except KeyError: os.write(2, 'bad color specifier "%s"\n' % (word,)) return ret[:-1] + 'm', attrOFF ### (ON,OFF) TERMINAL SEQUENCES def exists(path): try: os.stat(path) return 1 except: return 0 class File: #### ENCAPSULATE TYPED FILES def matcher(pat): return re.compile(pat).match patterns = { 'any' : '.*' } for var, val in env.items(): if len(var) > 8 and var[:8] == 'CLS_PAT_': patterns[var[8:]] = val for nm, pat in patterns.items(): exec ('pat_%s = matcher("%s")\n' + \ 'def %s(f): return File.pat_%s(f.name)') % (nm, pat, nm, nm) def symlink(f): return S_ISLNK(f.st[ST_MODE]) def directory(f): return S_ISDIR(f.st[ST_MODE]) def socket(f): return S_ISSOCK(f.st[ST_MODE]) or S_ISFIFO(f.st[ST_MODE]) def blockdev(f): return S_ISBLK(f.st[ST_MODE]) def chardev(f): return S_ISCHR(f.st[ST_MODE]) def executable(f): return f.st[ST_MODE] & (S_IXUSR|S_IXGRP|S_IXOTH) def brokenlink(f): return not exists(f.path) if 'CLS_COLOR_TYPE' in env: fmty = eval(env['CLS_COLOR_TYPE']) else: fmty = ((any, mkcolor(""))) fmtynm = map(lambda x: x[0].__name__, fmty) if 'CLS_ORDER' in env: ordy = eval(env['CLS_ORDER']) else: ordy = [ any ] ordynm = map(lambda x: x.__name__, ordy) for i in range(len(ordy)): if ordy[i] == brokenlink: ordbrok = i break else: ordbrok = len(ordy) - 1 for i in range(len(fmty)): if fmty[i] == brokenlink: fmtbrok = i break else: fmtbrok = len(fmty) - 1 def idx(f, preds): ## first index satisfying a predicate i = 0 for pred in preds: if pred(f): break i = i + 1 return i def idx2(f, preds): ## like idx(), but extract differently i = 0 for pred in preds: if pred[0](f): break i = i + 1 return i def colored(f, xfrm=lambda x: x): ### colorize a directory entry if use_color: on, off = File.fmty[f.fty][1] return on + xfrm(f.name) + off return xfrm(f.name) def __init__(f, name, path): ### construct a typed file object global symderef f.name = name f.path = path or name f.ext = splitext(path)[-1] try: if symderef: try: f.st = os.stat(path) except: f.st = os.lstat(path) else: f.st = os.lstat(path) f.oty = f.idx(File.ordy) # find type for ordering purposes f.fty = f.idx2(File.fmty) # find type for formatting purposes f.usr = Usr(f.st[ST_UID])[0] # XXX should only compute usr/grp f.grp = Grp(f.st[ST_GID])[0] # XXX if they are needed by format except: os.write(2, 'File:%s:%s\n'%(str(sys.exc_type), str(sys.exc_value))) f.st = [0,0,0,0,0,0,0,0,0,0] f.oty = ordbrok f.fty = fmtbrok f.usr = 'None' f.grp = 'None' f.cmp = [ ] ## cache projections of obj for cmp() for cmp in cmps: # XXX this should be much faster, f.cmp.append(cmp(f)) # XXX but isn't for some reason... class Link(File): def __init__(l, f): l.name = os.readlink(f.path) dn = dirname(f.path) # handle relative symlinks correctly if dn and dn != '.' and dn[0] != '/': l.path = dn + '/' + l.name else: l.path = l.name l.ext = splitext(l.path)[-1] try: l.st = os.stat(l.path) l.oty = l.idx(File.ordy) l.fty = l.idx2(File.fmty) l.usr = Usr(l.st[ST_UID])[0] l.grp = Grp(l.st[ST_GID])[0] except: os.write(2, 'Link:%s:%s\n'%(str(sys.exc_type), str(sys.exc_value))) l.st = [0,0,0,0,0,0,0,0,0,0] l.oty = ordbrok l.fty = fmtbrok l.usr = 'None' l.grp = 'None' def Usr(uid): #### FORMATTING FUNCTIONS try: return pwd.getpwuid(uid) except KeyError: return `uid` ### fall back to numeric id def Grp(gid): try: return grp.getgrgid(gid) except KeyError: return `gid` ### fall back to numeric id def size_rounder(bytes): ### express byte size as 4-char string K, M, G = pow(2.0,10), pow(2.0,20), pow(2.0,30) if bytes <= 9999: sz = '%4d' % bytes elif bytes < 99.5 * K: sz = '%3.2gK' % float(bytes/K) elif bytes < 100 * K: sz = '100K' elif bytes < 995 * K: sz = '%3.3gK' % float(bytes/K) elif bytes < .995 * M: sz = ('%3.2gM' % float(bytes/M)) [1:] elif bytes < 1024 * K: sz = '%3.2gM' % float(bytes/M) elif bytes < 99.5 * M: sz = '%3.2gM' % float(bytes/M) elif bytes < 100 * M: sz = '100M' elif bytes < 995 * M: sz = '%3.3gM' % float(bytes/M) elif bytes < .995 * G: sz = ('%3.2gG' % float(bytes/G)) [1:] elif bytes < 1024 * M: sz = '%3.2gG' % float(bytes/G) elif bytes < 99.5 * G: sz = '%3.2gG' % float(bytes/G) elif bytes < 100 * G: sz = '100G' else: sz = '%3.3gG' % float(bytes/G) k,m,g = mkcolor('bold yellow'), mkcolor('bold red'), mkcolor('bold purple') if sz[-1] == 'K': return k[0] + sz + k[1] if sz[-1] == 'M': return m[0] + sz + m[1] if sz[-1] == 'G': return g[0] + sz + g[1] return sz if use_color: pcolors = (mkcolor('bold red'), ## 0 = 000 = no perms mkcolor('yellow'), ## 1 = 001 = exec only mkcolor('bold blue'), ## 2 = 010 = write only mkcolor('green'), ## 3 = 011 = exec+write mkcolor('bold green'), ## 4 = 100 = read only mkcolor('bold yellow'), ## 5 = 101 = read+exec mkcolor('bold cyan'), ## 6 = 110 = read+write mkcolor('bold white')) ## 7 = 111 = read+write+exec my_groups = [ ] my_uid = os.getuid() my_usrnm = Usr(my_uid) for gr, pw, no, members in grp.getgrall(): if my_usrnm in members: my_groups.append(no) def pcolor(f): ### colorize a permission mask if use_color: mode = f.st[ST_MODE] if f.st[ST_UID] == my_uid: on, off = pcolors[mode >> 6 & 7] elif f.st[ST_GID] in my_groups: on, off = pcolors[mode >> 3 & 7] else: on, off = pcolors[mode & 7] return on + ('%04o' % (mode&4095,)) + attrOFF return path def Mtime(f): return max(f.st[ST_CTIME],f.st[ST_CTIME]) form = { ### key:(formatter, description) dict 'f': (lambda f: f.colored(), 'file name' ), 'F': (lambda f: f.colored(basename), 'base name' ), 'b': (lambda f: `f.st.st_blocks`, 'file blocks' ), 'i': (lambda f: `f.st[ST_INO]`, 'i-node number' ), 'n': (lambda f: `f.st[ST_NLINK]`, 'link count' ), 'S': (lambda f: `f.st[ST_SIZE]`[:-1], 'size' ), 's': (lambda f: (File.blockdev(f) or File.chardev(f)) and ("x%05X" % f.st.st_rdev) or size_rounder(f.st[ST_SIZE]), 'rnd size|devno'), 'd': (lambda f: `f.st.st_rdev & 0xFF`, 'minor dev num' ), 'D': (lambda f: `f.st.st_rdev >> 8`, 'major dev num' ), 'u': (lambda f: `f.st[ST_UID]`, 'user id' ), 'g': (lambda f: `f.st[ST_GID]`, 'group id' ), 'a': (lambda f: strftime(tfmt,localtime(f.st[ST_ATIME])), 'access time' ), 'm': (lambda f: strftime(tfmt,localtime(f.st[ST_MTIME])), 'modify time' ), 'c': (lambda f: strftime(tfmt,localtime(f.st[ST_CTIME])), 'create time' ), 'M': (lambda f: strftime(tfmt,localtime(Mtime(f))), 'max(ctm,mtm)' ), 'U': (lambda f: f.usr, 'user name' ), 'G': (lambda f: f.grp, 'group name' ), 'p': (lambda f: '%04o' % (f.st[ST_MODE]&4095,), 'permission' ), 'P': (lambda f: pcolor(f), 'color perms' ), 't': (lambda f: `f.oty`, 'order typeno' ), 'T': (lambda f: '%3.3s' % (File.fmtynm[f.fty],), 'format typenm' ), 'r': (lambda f: File.symlink(f) and '->'+os.readlink(f.path) or '','readlink' ), 'R': (lambda f: File.symlink(f) and '->'+Link(f).colored() or '', 'lnk w/color tgt') } def numericize(f): lst = re.split('([0-9]+)', f.name) for i in range(len(lst)): try: j = int(lst[i]) lst[i] = j except: pass return lst cmpP = { ### key:(compare projector, desc) dict 'f': (lambda f: f.name, 'file name' ), 'N': (numericize, 'numeric file names' ), 'F': (lambda f: basename(f.name), 'base name' ), 'b': (lambda f: f.st.st_blocks, 'file blocks' ), 'i': (lambda f: f.st[ST_INO], 'i-node number' ), 'n': (lambda f: f.st[ST_NLINK], 'link count' ), 's': (lambda f: f.st[ST_SIZE], 'size' ), 'd': (lambda f: f.st.st_rdev & 0xFF, 'minor dev num' ), 'D': (lambda f: f.st.st_rdev >> 8, 'major dev num' ), 'u': (lambda f: f.st[ST_UID], 'user id' ), 'g': (lambda f: f.st[ST_GID], 'group id' ), 'a': (lambda f: f.st[ST_ATIME], 'access time' ), 'm': (lambda f: f.st[ST_MTIME], 'modify time' ), 'c': (lambda f: f.st[ST_CTIME], 'create time' ), 'M': (lambda f: Mtime(f), 'max(ctime,mtime)' ), 'U': (lambda f: f.usr, 'user name' ), 'G': (lambda f: f.grp, 'group name' ), 'p': (lambda f: f.st[ST_MODE]&4095, 'permission' ), 't': (lambda f: ord_xlat[f.oty], 'order type number' ), 'T': (lambda f: f.fty, 'format type number' ), 'e': (lambda f: f.ext, 'filename extension' ), 'L': (lambda f: len(f.name), 'filename length' ) } def mkfmts(fmt): ### build form func list from fmt pfmt = list(fmt) # pfmt will be a %-format specifier fmts = [ ] # with %...? replaced by %...s i = 0 while i < len(fmt): if fmt[i] == '%': i = i + 1 if fmt[i] == '%': i = i + 1 continue while fmt[i] in '#0123456789+-. ': i = i + 1 try: fmts.append(form[fmt[i]][0]) pfmt[i] = 's' except KeyError: os.write(2, "unknown ls format specifier '%"+fmt[i]+"'\n") pfmt[i] = '%' # illegal -> a literal '%' in output i = i + 1 return fmts, string.join(pfmt, '') def mkcmps(orderspec): ### build cmpP func list from orderspec cs = [ ] if orderspec == '-': return cs sgn = +1 for ch in orderspec: if ch == '-': sgn = -1 continue try: cs.append(cmpP[ch][0]) cmp_sign.append(sgn) except KeyError: os.write(2, "unknown sort key specifier '" + ch + "'\n") sgn = +1 return cs def ftype_ord_mk(want): ### build translator array 'x' x = range(0, len(want)) j = 0 for i in want: x[i] = j j = j + 1 return x def format(f): ## DRIVE FORMAT fields = map(apply, fmts, ((f,),) * len(fmts)) return pfmt % tuple(fields) def multi_level_cmp(a, b): ## CACHED PROJECTION MULTI-LEVEL CMP for i in range(len(cmps)): val = cmp(a.cmp[i], b.cmp[i]) if val != 0: return cmp_sign[i] * val return 0 #import _psyco #multi_level_cmp = _psyco.proxy(multi_level_cmp, 0) def printlen(str): ### strip attr ESC sequences return len(re.sub('\x1b[^m]+m', '', str)) def ls(pfx, args): #### MAIN ENTRY POINT global ord_xlat fs = [ ] if not compress: ## basic case: construct typed fs for p in args: fs.append(File(p, pfx + p)) else: ## harder case: order types by space widthy = [ 0 ] * len(ord_xlat) # max terminal width per type for p in args: f = File(p, pfx + p) fs.append(f) if len(p) > widthy[f.oty]: # track max width(type) as we go widthy[f.oty] = len(p) want_ord = [ ] for i in range(0, len(widthy)): want_ord.append( (widthy[i], i) ) want_ord.sort() ord_xlat = ftype_ord_mk(map(lambda x: x[1], want_ord)) dirs = filter(File.directory, fs) ## TODO? sort if len(inctyO) or len(inctyF): fs = filter(lambda f: f.fty in inctyF or f.oty in inctyO, fs) else: fs = filter(lambda f: f.fty not in igntyF and f.oty not in igntyO, fs) if len(cmps) > 0: fs.sort(multi_level_cmp) ## sort listing strs = map(format, fs) ## format files os.write(1, table(strs, W, '', gap, maxcol, printlen)) return dirs def lsdir(d, r): ### labeling+recursing ls()-wrapper global dirlabel, nl try: entries = os.listdir(d.name) for ignorer in ignore: entries = filter(lambda x, f=ignorer: not f(x), entries) if dirlabel: if d.name[:2] == './': d.name = d.name[2:] os.write(1, nl + d.colored() + ':\n') dirlabel = 1 ## Always need dirlabels+line sep after nl = '\n' ## first time we *may* have needed one. if d.name[-1] != '/': d.name = d.name + '/' ds = ls(d.name, entries) if r: for de in ds: de.name = d.name + de.name lsdir(de, r - 1) except: os.write(2, 'lsdir(): %s: %s\n'%(str(sys.exc_type),str(sys.exc_value))) return [ ] def str2igntyF(a): for t in string.split(a, ','): igntyF.append(File.fmtynm.index(t)) def str2igntyO(a): for t in string.split(a, ','): igntyO.append(File.ordynm.index(t)) def str2inctyF(a): for t in string.split(a, ','): inctyO.append(File.fmtynm.index(t)) def str2inctyO(a): for t in string.split(a, ','): inctyO.append(File.ordynm.index(t)) def lscmd(): #### COMMAND LINE INTERFACE global W, fmt, ord, tfmt, gap, maxcol, compress, symderef, \ ord_xlat, ignore, inctyF, inctyO, igntyF, igntyO, \ fmts, pfmt, cmps, dirlabel,nl W = terminal_size()[0] ## set terminal width from getopt import getopt use=\ 'Usage:\n '+basename(sys.argv[0])+' OPTIONS [ FILES|DIRS ]\n' + \ '\t-{f,-format= } FMT_LS. FMT_LS has printf-like specifiers:\n' + \ table(map(lambda x:'%'+x[0]+' '+x[1][1], form.items()), W - 16, '\t\t')+\ '\t-{o,-order= } ORDER. ORDER = [-]KEY1[-]KEY2... for KEYn in:\n' + \ table(map(lambda x:x[0]+' '+x[1][1], cmpP.items()), W - 16, '\t\t')+\ '\t-{t,-time= } FMT_TM (FMT_TM is an strftime(3) format)\n' + \ '\t-{c,-includeOrd=} t1,t2,... only file order types t1, t2, ..\n' + \ '\t-{C,-includeFmt=} t1,t2,... only file format types t1, t2, ..\n' + \ '\t-{x,-xcludeOrd= } t1,t2,... omit file order types t1, t2, ..\n' + \ '\t-{X,-xcludeFmt= } t1,t2,... omit file format types t1, t2, ..\n' + \ '\t-{i,-ignore= } p1,p2,... omit file names matching regex\n' + \ '\t-{r,-recurse } N recurse N levels, 0=infinite\n' +\ '\t-{z,-compress } order types by size for more columns\n'+\ '\t-{d,-directory } list only directories, not contents\n'+\ '\t-{L,-dereference} dereference symlinks\n'+\ '\t-{p,-plain } text embellishment->off (default=on)\n'+\ '\t- use no more than N columns\n' try: ### parse CLI options opts,argv = getopt(sys.argv[1:], 'f:o:t:c:C:x:X:i:r:zdLp123456789', [ 'format=', 'sort=', 'order=', 'time=', 'includeFmt=', 'includeOrd=', 'xcludeFmt=', 'xcludeOrd=', 'ignore=', 'recursive=', 'compress', 'plain', 'directory', 'dereference' ]) except: os.write(2, use) ### bad options: write usage sys.exit(1) fmt = env.has_key('CLS_FORMAT') and env['CLS_FORMAT'] or '%f' ord = env.has_key('CLS_SORT') and env['CLS_SORT'] or 'tef' tfmt = env.has_key('CLS_TIME_FMT') and env['CLS_TIME_FMT'] or \ '\x1b[1;34m%y\x1b[1;36m%m\x1b[1;37m%d\x1b[0m' + \ '\x1b[1;34m%H\x1b[1;36m%M\x1b[1;37m%S\x1b[0m' gap = 2 ## deflt minimum gap maxcol = 999 ## deflt max columns compress = 0 ## deflt to using below as type order justdirs = 0 ## deflt to listing directory content recurse = 0 ## deflt levels to recurse. 0 => none symderef = 0 ## deflt to showing symlinks ord_xlat = range(0,len(File.ordy)+1) ## deflt to identity ignore = [ ] inctyO = [ ] inctyF = [ ] igntyO = [ ] igntyF = [ ] for o, a in opts: o = o[1:] # eat leading '-' if o in ('f', '-format' ): fmt = a elif o in ('o', '-sort', '-order'): ord = a elif o in ('t', '-time' ): tfmt = a elif o in ('c', '-includeOrd' ): str2inctyO(a) elif o in ('C', '-includeFmt' ): str2inctyF(a) elif o in ('x', '-xcludeOrd' ): str2igntyO(a) elif o in ('X', '-xcludeFmt' ): str2igntyF(a) elif o in ('i', '-ignore' ): ignore.append(a) elif o in ('r', '-recursive' ): recurse = -(int(a) + 1) elif o in ('z', '-compress' ): compress = 1 elif o in ('d', '-directory' ): justdirs = 1 elif o in ('L', '-dereference' ): symderef = 1 elif o in ('p', '-plain' ): use_color = 0 elif o in ('1','2','3','4','5','6','7','8','9'): maxcol = int(o) if recurse < -1: recurse = -1 - recurse fmts, pfmt = mkfmts(fmt) cmps = mkcmps(ord) ignore = map(lambda x: re.compile(x).match, ignore) if not len(argv): argv = [ '.' ] if justdirs: # NOTE: => no recursion ls('', argv) else: str2igntyF('directory') # temp append dir to ignty to not dirs = ls('', argv) # list dirs specifically named in argv igntyF = igntyF[:-1] # Then revert igntyF. nl = '\n' dirlabel = 1 # dirlabel in lsdir() if len(dirs) == len(argv): ## all argv are directories nl = '' # Elide first \n if argv is all dirs if not recurse and len(dirs) == 1: dirlabel = 0 # no label when argv is exactly 1 dir for d in dirs: lsdir(d, recurse) if __name__ == '__main__': try: import psyco psyco.jit(1) psyco.bind(File) except: pass lscmd()