-
Notifications
You must be signed in to change notification settings - Fork 33
/
lsx
executable file
·135 lines (117 loc) · 5.14 KB
/
lsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
#!/usr/bin/env python
import os, sys, stat, re, pathlib as pl, collections as cs
p_err = lambda *a,**kw: print('ERROR:', *a, **kw, file=sys.stderr, flush=True)
def list_adjacent(paths, specs, files_only=False):
p_dirs, stats, p_out = dict(), dict(), dict()
op_out_sort = None
for p in paths:
try: st = stats[p] = p.stat()
except OSError: p_err(f'path inaccessible [ {p} ]'); continue
ps = set(p.parent.iterdir())
if files_only:
if not stat.S_ISREG(st.st_mode): continue # arg path discarded
for pa in list(ps):
try: st = stats[pa] = pa.stat()
except OSError: ps.remove(pa)
if not stat.S_ISREG(st.st_mode): ps.remove(pa)
p_out[p], p_dirs[p] = {p}, tuple(ps)
for opt in (opt.lower() for opt in specs):
if n := re.findall(r'(\d+)', opt): n = max(map(int, n))
else: n = 10
ops = set(opt).difference('1234567890')
if ops.difference('bants'):
parser.error(f'Unrecognized letters in -a/--adjacent spec: {opt!r}')
if ('a' in ops) ^ ('b' in ops) == 0: na = nb = n
elif 'a' in ops: na, nb = n, 0
elif 'b' in ops: na, nb = 0, n
ops.difference_update('ab')
if not ops: ops.add('n')
elif not op_out_sort:
op_out_sort = sorted(ops, key=lambda k: opt.find(k))[0]
p_lists = cs.defaultdict(set) # {p: set(ps_sorted, ...), ...}
for p, ps in p_dirs.items():
if 'n' in ops:
p_lists[p].add(tuple(sorted(ps, key=lambda pa: pa.name)))
if ops.intersection('ts'):
for pa in ps:
if pa in stats: continue
try: stats[pa] = pa.stat()
except OSError: pass
for k, sk in ('t', 'st_mtime'), ('s', 'st_size'):
if k not in ops: continue
p_lists[p].add(tuple(sorted( ps,
key=lambda pa: getattr(stats[pa], sk) )))
for p, pss in p_lists.items():
for ps in pss:
try: n = ps.index(p)
except ValueError: p_err(f'path vanished [ {p} ]'); continue
p_out[p].update(ps[max(0, n-nb):n])
p_out[p].update(ps[n+1:n+na+1])
ps_print, sort_func = dict(), dict(
n=lambda p: p.name,
t=lambda p: stats[p].st_mtime,
s=lambda p: stats[p].st_size )[op_out_sort or 'n']
for p, ps in p_out.items():
p_out[p] = sorted(ps, key=sort_func)
for ps in p_out.values():
for p in ps:
if p in ps_print: continue
if not (st := stats.get(p)):
try: st = stats[p] = p.stat()
except OSError: continue
ps_print[p] = None
return list(ps_print.keys())
def main(argv=None):
import argparse, textwrap
dd = lambda text: re.sub( r' \t+', ' ',
textwrap.dedent(text).strip('\n') + '\n' ).replace('\t', ' ')
parser = argparse.ArgumentParser(
formatter_class=argparse.RawTextHelpFormatter,
usage='%(prog)s [opts] [[--] file/dir...]',
description='Custom functionality extensions of the common "ls" tool.')
parser.add_argument('paths', nargs='*', help=dd('''
File/dir path arguments for various options below.
If no other options are provided, tool will simply list all existing
path(s), printing errors on stderr for those that can't be accessed.'''))
parser.add_argument('-f', '--files', action='store_true',
help='Print only files, not dirs or any other stuff.')
parser.add_argument('-0', '--null', action='store_true',
help='Print null-terminated list, instead of default newline-terminated.')
parser.add_argument('-a', '--adjacent', metavar='opts', action='append', help=dd('''
Show specified file/dir path, and also N files/dirs adjacent to it.
"Adjacent" as in located within same directory, and sorted right before/after it.
Requires a parameter, consisting of optional numbers/letters:
number (digits) - count of items (default=10) to display before/after path(s),
counting before/after separately (if enabled), and not counting path itself.
b/B - before - only list entries sorted before specified one(s).
a/A - after - same as "before" above, can be combined for before + after (default).
n/N - name - order items by filenames alphabetically (default).
t/T - time - order items by mtime.
s/S - size - order items by apparent filesize (from stat.st_size).
Default is to return 10 before/after items with alphabetical ordering.
Some file/dir arguments must be specified for this option.
Option can be used multiple times to select more items by additional criteria,
in which case they will be returned sorted by the first
parameter that's explicitly specified (or default alpha-sorted otherwise).'''))
opts = parser.parse_args(sys.argv[1:] if argv is None else argv)
pp_first, pp_nullsep = True, opts.null
def _pp(p):
nonlocal pp_first
p, (cc, cn) = str(p), ('\n', 'newline') if not pp_nullsep else ('\0', 'null-char')
if cc in p: p_err(f'Skipping entry with {cn} in it: {p!r}')
if not pp_nullsep: p = f'{p}\n'
elif not pp_first: p = f'\0{p}'
else: pp_first = False
sys.stdout.write(p)
paths = list(pl.Path(p) for p in opts.paths)
if opts.adjacent:
for p in list_adjacent(paths, opts.adjacent, opts.files): _pp(p)
else: # no options - print filtered input paths back
for p in paths:
if not p.exists(): p_err(f'path inaccessible [ {p} ]'); continue
if not opts.files or p.is_file(): _pp(p)
if __name__ == '__main__':
try: sys.exit(main())
except BrokenPipeError: # stdout pipe closed
os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno())
sys.exit(1)