#!/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-<N>                        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()
