skilleter-thingy 0.3.14__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- skilleter_thingy/__init__.py +0 -0
- skilleter_thingy/addpath.py +107 -0
- skilleter_thingy/console_colours.py +63 -0
- skilleter_thingy/ffind.py +535 -0
- skilleter_thingy/ggit.py +88 -0
- skilleter_thingy/ggrep.py +155 -0
- skilleter_thingy/git_br.py +186 -0
- skilleter_thingy/git_ca.py +147 -0
- skilleter_thingy/git_cleanup.py +297 -0
- skilleter_thingy/git_co.py +227 -0
- skilleter_thingy/git_common.py +68 -0
- skilleter_thingy/git_hold.py +162 -0
- skilleter_thingy/git_parent.py +84 -0
- skilleter_thingy/git_retag.py +67 -0
- skilleter_thingy/git_review.py +1450 -0
- skilleter_thingy/git_update.py +398 -0
- skilleter_thingy/git_wt.py +72 -0
- skilleter_thingy/gitcmp_helper.py +328 -0
- skilleter_thingy/gitprompt.py +293 -0
- skilleter_thingy/linecount.py +154 -0
- skilleter_thingy/multigit.py +915 -0
- skilleter_thingy/py_audit.py +133 -0
- skilleter_thingy/remdir.py +127 -0
- skilleter_thingy/rpylint.py +98 -0
- skilleter_thingy/strreplace.py +82 -0
- skilleter_thingy/test.py +34 -0
- skilleter_thingy/tfm.py +948 -0
- skilleter_thingy/tfparse.py +101 -0
- skilleter_thingy/trimpath.py +82 -0
- skilleter_thingy/venv_create.py +47 -0
- skilleter_thingy/venv_template.py +47 -0
- skilleter_thingy/xchmod.py +124 -0
- skilleter_thingy/yamlcheck.py +89 -0
- skilleter_thingy-0.3.14.dist-info/METADATA +606 -0
- skilleter_thingy-0.3.14.dist-info/RECORD +39 -0
- skilleter_thingy-0.3.14.dist-info/WHEEL +5 -0
- skilleter_thingy-0.3.14.dist-info/entry_points.txt +31 -0
- skilleter_thingy-0.3.14.dist-info/licenses/LICENSE +619 -0
- skilleter_thingy-0.3.14.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
#! /usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
################################################################################
|
|
4
|
+
""" Really simple file find utility
|
|
5
|
+
|
|
6
|
+
Implements the functionality of the find command that is regularly used
|
|
7
|
+
in a simpler fashion and ignores all the options that nobody ever uses.
|
|
8
|
+
|
|
9
|
+
Copyright (C) 2018 John Skilleter
|
|
10
|
+
|
|
11
|
+
TODO: Option for Partial matching - x matches '*x*'
|
|
12
|
+
TODO: Mixed case matching - lower case search matches case-independently - if search contains upper case then it is case-dependent
|
|
13
|
+
TODO: Optional hidden hyphen/underscore detection - 'xyz' matches '_x-y_z-'
|
|
14
|
+
TODO: Option to Use .gitignore
|
|
15
|
+
TODO: More checks on --exec parameter - only one '^', check quoting.
|
|
16
|
+
"""
|
|
17
|
+
################################################################################
|
|
18
|
+
|
|
19
|
+
import sys
|
|
20
|
+
import argparse
|
|
21
|
+
import os
|
|
22
|
+
import fnmatch
|
|
23
|
+
import stat
|
|
24
|
+
import grp
|
|
25
|
+
import pwd
|
|
26
|
+
import datetime
|
|
27
|
+
import re
|
|
28
|
+
import shlex
|
|
29
|
+
import logging
|
|
30
|
+
import subprocess
|
|
31
|
+
|
|
32
|
+
from skilleter_modules import git
|
|
33
|
+
from skilleter_modules import dircolors
|
|
34
|
+
from skilleter_modules import colour
|
|
35
|
+
|
|
36
|
+
################################################################################
|
|
37
|
+
|
|
38
|
+
def error(msg, status=1):
|
|
39
|
+
""" Report an error message and exit """
|
|
40
|
+
|
|
41
|
+
sys.stderr.write(f'{msg}\n')
|
|
42
|
+
sys.exit(status)
|
|
43
|
+
|
|
44
|
+
################################################################################
|
|
45
|
+
|
|
46
|
+
def report_exception(exc):
|
|
47
|
+
""" Handle an exception triggered inside os.walk - currently just reports
|
|
48
|
+
the exception - typically a permission error. """
|
|
49
|
+
|
|
50
|
+
sys.stderr.write(f'{exc}\n')
|
|
51
|
+
|
|
52
|
+
################################################################################
|
|
53
|
+
|
|
54
|
+
def report_file(args, filepath, filestat, dircolour):
|
|
55
|
+
""" Report details of a file or directory in the appropriate format """
|
|
56
|
+
|
|
57
|
+
# If we are just counting files we don't report anything
|
|
58
|
+
|
|
59
|
+
if args.count_only:
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
if args.abspath:
|
|
63
|
+
filepath = os.path.abspath(filepath)
|
|
64
|
+
|
|
65
|
+
# Either run the exec command on the file and/or report it
|
|
66
|
+
|
|
67
|
+
if args.exec:
|
|
68
|
+
# Copy the exec string and insert the file
|
|
69
|
+
|
|
70
|
+
cmd = []
|
|
71
|
+
for i in args.exec:
|
|
72
|
+
cmd.append(i.replace('^', filepath))
|
|
73
|
+
|
|
74
|
+
logging.debug('Running "%s"', ' '.join(cmd))
|
|
75
|
+
|
|
76
|
+
# Attempt to run the command
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
subprocess.run(cmd, check=True)
|
|
80
|
+
except subprocess.CalledProcessError as exc:
|
|
81
|
+
error('%s failed with status=%d' % (' '.join(cmd), exc.returncode))
|
|
82
|
+
except FileNotFoundError:
|
|
83
|
+
error('File not found attempting to run "%s"' % ' '.join(cmd))
|
|
84
|
+
except PermissionError:
|
|
85
|
+
error('Permission error attempting to run "%s"' % ' '.join(cmd))
|
|
86
|
+
|
|
87
|
+
if args.verbose or not args.exec:
|
|
88
|
+
if args.zero:
|
|
89
|
+
sys.stdout.write(f'{filepath}\0')
|
|
90
|
+
else:
|
|
91
|
+
# Colourise output if required
|
|
92
|
+
|
|
93
|
+
if dircolour:
|
|
94
|
+
filepath = dircolour.format(filepath)
|
|
95
|
+
|
|
96
|
+
# Quote the file if necessary
|
|
97
|
+
|
|
98
|
+
if not args.unquoted and ' ' in filepath:
|
|
99
|
+
filepath = f'"{filepath}"'
|
|
100
|
+
|
|
101
|
+
# Output full details or just the filename
|
|
102
|
+
|
|
103
|
+
if args.long:
|
|
104
|
+
filedate = datetime.datetime.fromtimestamp(filestat[stat.ST_MTIME])
|
|
105
|
+
|
|
106
|
+
print('%s %-10s %-10s %8d %-10s %s' %
|
|
107
|
+
(stat.filemode(filestat.st_mode),
|
|
108
|
+
pwd.getpwuid(filestat[stat.ST_UID])[0],
|
|
109
|
+
grp.getgrgid(filestat[stat.ST_GID])[0],
|
|
110
|
+
filestat[stat.ST_SIZE],
|
|
111
|
+
filedate,
|
|
112
|
+
filepath))
|
|
113
|
+
else:
|
|
114
|
+
print(filepath)
|
|
115
|
+
|
|
116
|
+
################################################################################
|
|
117
|
+
|
|
118
|
+
def ismatch(args, root, file):
|
|
119
|
+
""" Return True if pattern is a match for the current search parameters
|
|
120
|
+
(based on name, not type) """
|
|
121
|
+
|
|
122
|
+
file = os.path.join(root, file) if args.fullpath else file
|
|
123
|
+
|
|
124
|
+
check_file = file.lower() if args.iname else file
|
|
125
|
+
|
|
126
|
+
for pattern in args.patterns:
|
|
127
|
+
if args.regex:
|
|
128
|
+
if pattern.match(check_file):
|
|
129
|
+
return True
|
|
130
|
+
else:
|
|
131
|
+
check_pattern = pattern.lower() if args.iname else pattern
|
|
132
|
+
|
|
133
|
+
if fnmatch.fnmatch(check_file, check_pattern):
|
|
134
|
+
return True
|
|
135
|
+
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
################################################################################
|
|
139
|
+
|
|
140
|
+
def istype(args, filestat):
|
|
141
|
+
""" Return True if the stat data is for an entity we are interested in """
|
|
142
|
+
|
|
143
|
+
return args.any or \
|
|
144
|
+
(args.file and stat.S_ISREG(filestat.st_mode)) or \
|
|
145
|
+
(args.block and stat.S_ISBLK(filestat.st_mode)) or \
|
|
146
|
+
(args.char and stat.S_ISCHR(filestat.st_mode)) or \
|
|
147
|
+
(args.pipe and stat.S_ISFIFO(filestat.st_mode)) or \
|
|
148
|
+
(args.symlink and stat.S_ISLNK(filestat.st_mode)) or \
|
|
149
|
+
(args.socket and stat.S_ISSOCK(filestat.st_mode))
|
|
150
|
+
|
|
151
|
+
################################################################################
|
|
152
|
+
|
|
153
|
+
def git_ffind(args, dircolour):
|
|
154
|
+
""" Do the finding in the git working tree """
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
files = git.files(dir=args.path)
|
|
158
|
+
except git.GitError as exc:
|
|
159
|
+
error(exc.msg, exc.status)
|
|
160
|
+
|
|
161
|
+
files.sort()
|
|
162
|
+
found = []
|
|
163
|
+
|
|
164
|
+
if files:
|
|
165
|
+
# We normally follow symlinks, unless we're searching for them
|
|
166
|
+
|
|
167
|
+
follow = args.follow and not args.symlink
|
|
168
|
+
|
|
169
|
+
for file in files:
|
|
170
|
+
root, filename = os.path.split(file)
|
|
171
|
+
|
|
172
|
+
if ismatch(args, root, filename):
|
|
173
|
+
filestat = os.stat(file, follow_symlinks=follow)
|
|
174
|
+
|
|
175
|
+
if istype(args, filestat):
|
|
176
|
+
report_file(args, file, filestat, dircolour)
|
|
177
|
+
found.append(file)
|
|
178
|
+
|
|
179
|
+
return found
|
|
180
|
+
|
|
181
|
+
################################################################################
|
|
182
|
+
|
|
183
|
+
def grep_match(regex, filename):
|
|
184
|
+
""" Return True if the specified file contains a match with the specified
|
|
185
|
+
regular expression """
|
|
186
|
+
|
|
187
|
+
recomp = re.compile(regex)
|
|
188
|
+
|
|
189
|
+
with open(filename, 'r', errors='ignore', encoding='utf8') as infile:
|
|
190
|
+
for text in infile:
|
|
191
|
+
if recomp.search(text):
|
|
192
|
+
break
|
|
193
|
+
else:
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
return True
|
|
197
|
+
|
|
198
|
+
################################################################################
|
|
199
|
+
|
|
200
|
+
def find_stuff(args, dircolour):
|
|
201
|
+
""" Do the finding """
|
|
202
|
+
|
|
203
|
+
found = []
|
|
204
|
+
|
|
205
|
+
# Recurse through the tree
|
|
206
|
+
|
|
207
|
+
for root, dirs, files in os.walk(args.path,
|
|
208
|
+
onerror=None if args.quiet else report_exception,
|
|
209
|
+
followlinks=args.follow):
|
|
210
|
+
|
|
211
|
+
logging.debug('Root=%s', root)
|
|
212
|
+
logging.debug('Dirs=%s', dirs)
|
|
213
|
+
logging.debug('Files=%s', files)
|
|
214
|
+
|
|
215
|
+
if root[0:2] == './':
|
|
216
|
+
root = root[2:]
|
|
217
|
+
elif root == '.':
|
|
218
|
+
root = ''
|
|
219
|
+
|
|
220
|
+
# We normally follow symlinks, unless we're searching for them
|
|
221
|
+
|
|
222
|
+
follow = args.follow and not args.symlink
|
|
223
|
+
|
|
224
|
+
# Sort the directories and files so that they are reported in order
|
|
225
|
+
|
|
226
|
+
dirs.sort()
|
|
227
|
+
files.sort()
|
|
228
|
+
|
|
229
|
+
# If not just searching for directories, check for a match in the files
|
|
230
|
+
|
|
231
|
+
if not args.dironly:
|
|
232
|
+
# Iterate through the files and patterns, looking for a match
|
|
233
|
+
|
|
234
|
+
for file in files:
|
|
235
|
+
is_match = ismatch(args, root, file)
|
|
236
|
+
if (args.invert and not is_match) or (not args.invert and is_match):
|
|
237
|
+
filepath = os.path.join(root, file)
|
|
238
|
+
filestat = os.stat(filepath, follow_symlinks=follow)
|
|
239
|
+
|
|
240
|
+
if istype(args, filestat):
|
|
241
|
+
# If the grep option is in use and this is a file and it doesn't contain a match
|
|
242
|
+
# just continue and don't report it.
|
|
243
|
+
|
|
244
|
+
if args.grep and (stat.S_ISREG(filestat.st_mode) or stat.S_ISLNK(filestat.st_mode)):
|
|
245
|
+
if not grep_match(args.grep, filepath):
|
|
246
|
+
continue
|
|
247
|
+
|
|
248
|
+
report_file(args, filepath, filestat, dircolour)
|
|
249
|
+
|
|
250
|
+
found.append(filepath)
|
|
251
|
+
|
|
252
|
+
# If searching for directories, check for a match there
|
|
253
|
+
|
|
254
|
+
if args.dir:
|
|
255
|
+
# Iterate through the directories and patterns looking for a match
|
|
256
|
+
|
|
257
|
+
for dirname in dirs:
|
|
258
|
+
is_match = ismatch(args, root, dirname)
|
|
259
|
+
if (args.invert and not is_match) or (not args.invert and is_match):
|
|
260
|
+
dirpath = os.path.join(root, dirname)
|
|
261
|
+
dirstat = os.stat(dirpath, follow_symlinks=follow)
|
|
262
|
+
report_file(args, dirpath, dirstat, dircolour)
|
|
263
|
+
found.append(dirpath)
|
|
264
|
+
|
|
265
|
+
# Prune directories that we aren't interested in so that we don't recurse into them
|
|
266
|
+
# but they can still be found themselves (so 'ffind -d .git' will find the .git directory
|
|
267
|
+
# even though it doesn't enter it.
|
|
268
|
+
|
|
269
|
+
if not args.all:
|
|
270
|
+
if '.git' in dirs:
|
|
271
|
+
dirs.remove('.git')
|
|
272
|
+
|
|
273
|
+
return found
|
|
274
|
+
|
|
275
|
+
################################################################################
|
|
276
|
+
|
|
277
|
+
def validate_arguments(args):
|
|
278
|
+
""" Validate and sanitise the command line arguments """
|
|
279
|
+
|
|
280
|
+
# Enable logging
|
|
281
|
+
|
|
282
|
+
if args.debug:
|
|
283
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
284
|
+
logging.debug('Debug logging enabled')
|
|
285
|
+
|
|
286
|
+
# Report conflicting options
|
|
287
|
+
|
|
288
|
+
if args.zero and args.long:
|
|
289
|
+
error('The zero and long options cannot be used together')
|
|
290
|
+
|
|
291
|
+
if args.human_readable:
|
|
292
|
+
error('Sorry - the -h/--human-readable option has not been implemented yet')
|
|
293
|
+
|
|
294
|
+
if (args.exec and (args.diff or args.long or args.zero or args.colour)):
|
|
295
|
+
error('The exec and formatting options cannot be used together')
|
|
296
|
+
|
|
297
|
+
# If we have a --type option validate the types.
|
|
298
|
+
|
|
299
|
+
if args.type:
|
|
300
|
+
for t in args.type:
|
|
301
|
+
if t not in ['b', 'c', 'd', 'p', 'f', 'l', 's']:
|
|
302
|
+
error(f'Invalid type "{t}"')
|
|
303
|
+
|
|
304
|
+
# Precompile regexes if using them
|
|
305
|
+
|
|
306
|
+
if args.regex:
|
|
307
|
+
regexes = []
|
|
308
|
+
flags = re.IGNORECASE if args.iname else 0
|
|
309
|
+
|
|
310
|
+
for pat in args.patterns:
|
|
311
|
+
try:
|
|
312
|
+
regexes.append(re.compile(pat, flags))
|
|
313
|
+
except re.error as exc:
|
|
314
|
+
error(f'Invalid regular expression "{pat}": {exc}')
|
|
315
|
+
|
|
316
|
+
args.patterns = regexes
|
|
317
|
+
|
|
318
|
+
# Convert the type entries (if any) into individual options (which are easier to check)
|
|
319
|
+
|
|
320
|
+
if args.type:
|
|
321
|
+
if 'b' in args.type:
|
|
322
|
+
args.block = True
|
|
323
|
+
|
|
324
|
+
if 'c' in args.type:
|
|
325
|
+
args.char = True
|
|
326
|
+
|
|
327
|
+
if 'd' in args.type:
|
|
328
|
+
args.dir = True
|
|
329
|
+
|
|
330
|
+
if 'p' in args.type:
|
|
331
|
+
args.pipe = True
|
|
332
|
+
|
|
333
|
+
if 'f' in args.type:
|
|
334
|
+
args.file = True
|
|
335
|
+
|
|
336
|
+
if 'l' in args.type:
|
|
337
|
+
args.symlink = True
|
|
338
|
+
|
|
339
|
+
if 's' in args.type:
|
|
340
|
+
args.socket = True
|
|
341
|
+
|
|
342
|
+
# Default to searching for filename matches
|
|
343
|
+
|
|
344
|
+
if not (args.block or args.char or args.dir or args.pipe or args.file or args.symlink or args.socket):
|
|
345
|
+
args.file = True
|
|
346
|
+
|
|
347
|
+
if not args.file and args.diff:
|
|
348
|
+
error('The "diff" option only works with files')
|
|
349
|
+
|
|
350
|
+
# Set a flag to indicate that we are only searching for directories
|
|
351
|
+
|
|
352
|
+
args.dironly = args.dir and not (args.any or args.block or args.char or args.pipe or args.file or args.symlink or args.socket)
|
|
353
|
+
|
|
354
|
+
if args.verbose:
|
|
355
|
+
if args.dironly:
|
|
356
|
+
print('Searching for directories only')
|
|
357
|
+
elif args.any:
|
|
358
|
+
print('Searching for any match')
|
|
359
|
+
else:
|
|
360
|
+
print('Searching for:')
|
|
361
|
+
|
|
362
|
+
print(' block devices : %s' % ('y' if args.block else 'n'))
|
|
363
|
+
print(' character devices: %s' % ('y' if args.char else 'n'))
|
|
364
|
+
print(' directories : %s' % ('y' if args.dir else 'n'))
|
|
365
|
+
print(' pipes : %s' % ('y' if args.pipe else 'n'))
|
|
366
|
+
print(' regular files : %s' % ('y' if args.file else 'n'))
|
|
367
|
+
print(' symlinks : %s' % ('y' if args.symlink else 'n'))
|
|
368
|
+
print(' sockets : %s' % ('y' if args.socket else 'n'))
|
|
369
|
+
|
|
370
|
+
# If we have the iname option convert the patterns to lower case
|
|
371
|
+
|
|
372
|
+
if args.iname and not args.regex:
|
|
373
|
+
lc_patterns = []
|
|
374
|
+
for pattern in args.patterns:
|
|
375
|
+
lc_patterns.append(pattern.lower())
|
|
376
|
+
|
|
377
|
+
args.patterns = lc_patterns
|
|
378
|
+
|
|
379
|
+
# If the git option has been specified, check that we are in a git working tree
|
|
380
|
+
|
|
381
|
+
if args.git:
|
|
382
|
+
if git.working_tree() is None:
|
|
383
|
+
colour.error('The current directory is not inside a git working tree', prefix=True)
|
|
384
|
+
|
|
385
|
+
if args.dir:
|
|
386
|
+
colour.error('Git does not track directories, so you cannot search for them in a git working tree', prefix=True)
|
|
387
|
+
|
|
388
|
+
if args.verbose:
|
|
389
|
+
print(f'Searching directory "{args.path}" for matches with "{args.patterns}"')
|
|
390
|
+
|
|
391
|
+
# If the exec option is used, convert the exec string to an array and add the '^' parameter
|
|
392
|
+
# marker if it isn't there.
|
|
393
|
+
|
|
394
|
+
if args.exec:
|
|
395
|
+
replacements = args.exec.count('^')
|
|
396
|
+
|
|
397
|
+
if replacements > 1:
|
|
398
|
+
error('Too many "^" characters in the exec string')
|
|
399
|
+
elif not replacements:
|
|
400
|
+
args.exec = f'{args.exec} ^'
|
|
401
|
+
|
|
402
|
+
args.exec = shlex.split(args.exec)
|
|
403
|
+
|
|
404
|
+
# If the path option has been used, try to switch to the specified directory
|
|
405
|
+
|
|
406
|
+
if args.path:
|
|
407
|
+
if not os.path.isdir(args.path):
|
|
408
|
+
error(f'"{args.path}" is not a directory')
|
|
409
|
+
|
|
410
|
+
os.chdir(args.path)
|
|
411
|
+
|
|
412
|
+
# Default if no patterns specified
|
|
413
|
+
|
|
414
|
+
if not args.patterns:
|
|
415
|
+
args.patterns = ['*']
|
|
416
|
+
|
|
417
|
+
return args
|
|
418
|
+
|
|
419
|
+
################################################################################
|
|
420
|
+
|
|
421
|
+
def parse_command_line():
|
|
422
|
+
""" Command line arguments """
|
|
423
|
+
|
|
424
|
+
parser = argparse.ArgumentParser(description='Find files, symlinks, directories, etc. according to criteria')
|
|
425
|
+
|
|
426
|
+
# General options
|
|
427
|
+
|
|
428
|
+
parser.add_argument('--path', '-p', action='store', default='.', help='Search the specified path, rather than the current directory')
|
|
429
|
+
parser.add_argument('--long', '-l', action='store_true', help='Output details of any files that match (cannot be used with -0/--zero)')
|
|
430
|
+
parser.add_argument('--colour', '-C', '--color', action='store_true', help='Colourise output even if not outputting to the terminal')
|
|
431
|
+
parser.add_argument('--no-colour', '-N', '--no-color', action='store_true', help='Never colourise output')
|
|
432
|
+
parser.add_argument('--all', action='store_true', help='Search all directories (do not skip .git, and similar control directories)')
|
|
433
|
+
parser.add_argument('--zero', '-0', action='store_true', help='Output results separated by NUL characters')
|
|
434
|
+
parser.add_argument('--iname', '-i', action='store_true', help='Perform case-independent search')
|
|
435
|
+
parser.add_argument('--follow', '-F', action='store_true', help='Follow symlinks')
|
|
436
|
+
parser.add_argument('--git', '-g', action='store_true', help='Only search for objects in the current git repository')
|
|
437
|
+
parser.add_argument('--diff', '-D', '--diffuse', action='store_true', help='Run Diffuse to on all the found objects (files only)')
|
|
438
|
+
parser.add_argument('--regex', '-R', action='store_true', help='Use regex matching rather than globbing')
|
|
439
|
+
parser.add_argument('--fullpath', '-P', action='store_true', help='Match the entire path, rather than just the filename')
|
|
440
|
+
parser.add_argument('--human-readable', '-H', action='store_true', help='When reporting results in long format, use human-readable sizes')
|
|
441
|
+
parser.add_argument('--grep', '-G', action='store', help='Only report files that contain text that matches the specified regular expression')
|
|
442
|
+
parser.add_argument('--abspath', '-A', action='store_true', help='Report the absolute path to matching entities, rather than the relative path')
|
|
443
|
+
parser.add_argument('--unquoted', '-U', action='store_true', help='Do not use quotation marks around results containing spaces')
|
|
444
|
+
parser.add_argument('--quiet', '-q', action='store_true', help='Do not report permission errors that prevented a complete search')
|
|
445
|
+
parser.add_argument('--invert', '-I', action='store_true', help='Invert the wildcard - list files that do not match')
|
|
446
|
+
parser.add_argument('--exec', '-x', action='store', help='Execute the specified command on each match (optionally use ^ to mark the position of the filename)')
|
|
447
|
+
parser.add_argument('--count', '-K', action='store_true', help='Report the number of objects found')
|
|
448
|
+
parser.add_argument('--count-only', '-c', action='store_true', help='Just report the number of objects found')
|
|
449
|
+
|
|
450
|
+
# Types of objects to include in the results
|
|
451
|
+
|
|
452
|
+
parser.add_argument('--type', '-t', action='append',
|
|
453
|
+
help='Type of item(s) to include in the results, where b=block device, c=character device, d=directory, p=pipe, f=file, l=symlink, s=socket. Defaults to files and directories')
|
|
454
|
+
parser.add_argument('--file', '-f', action='store_true', help='Include files in the results (the default if no other type specified)')
|
|
455
|
+
parser.add_argument('--dir', '-d', action='store_true', help='Include directories in the results')
|
|
456
|
+
parser.add_argument('--block', action='store_true', help='Include block devices in the results')
|
|
457
|
+
parser.add_argument('--char', action='store_true', help='Include character devices in the results')
|
|
458
|
+
parser.add_argument('--pipe', action='store_true', help='Include pipes in the results')
|
|
459
|
+
parser.add_argument('--symlink', '--link', action='store_true', help='Include symbolic links in the results')
|
|
460
|
+
parser.add_argument('--socket', action='store_true', help='Include sockets in the results')
|
|
461
|
+
parser.add_argument('--any', '-a', action='store_true', help='Include all types of item (the default unless specific types specified)')
|
|
462
|
+
|
|
463
|
+
# Debug
|
|
464
|
+
|
|
465
|
+
parser.add_argument('--verbose', '-v', action='store_true', help='Output verbose data')
|
|
466
|
+
parser.add_argument('--debug', action='store_true', help='Output debug data')
|
|
467
|
+
|
|
468
|
+
# Arguments
|
|
469
|
+
|
|
470
|
+
parser.add_argument('patterns', nargs='*', help='List of things to search for.')
|
|
471
|
+
|
|
472
|
+
args = parser.parse_args()
|
|
473
|
+
|
|
474
|
+
return validate_arguments(args)
|
|
475
|
+
|
|
476
|
+
################################################################################
|
|
477
|
+
|
|
478
|
+
def main():
|
|
479
|
+
""" Main function """
|
|
480
|
+
|
|
481
|
+
args = parse_command_line()
|
|
482
|
+
|
|
483
|
+
# Set up a dircolors intance, if one is needed
|
|
484
|
+
|
|
485
|
+
dircolour = dircolors.Dircolors() if args.colour or (sys.stdout.isatty() and not args.no_colour) else None
|
|
486
|
+
|
|
487
|
+
# Do the find!
|
|
488
|
+
|
|
489
|
+
if args.git:
|
|
490
|
+
files = git_ffind(args, dircolour)
|
|
491
|
+
else:
|
|
492
|
+
files = find_stuff(args, dircolour)
|
|
493
|
+
|
|
494
|
+
# Run diffuse, if required
|
|
495
|
+
|
|
496
|
+
if files:
|
|
497
|
+
if args.diff:
|
|
498
|
+
diff_cmd = ['diffuse']
|
|
499
|
+
|
|
500
|
+
for file in files:
|
|
501
|
+
if os.path.isfile(file):
|
|
502
|
+
diff_cmd.append(file)
|
|
503
|
+
|
|
504
|
+
try:
|
|
505
|
+
subprocess.run(diff_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
|
|
506
|
+
except subprocess.CalledProcessError as exc:
|
|
507
|
+
error(f'Diff failed with status={exc.returncode}')
|
|
508
|
+
except FileNotFoundError:
|
|
509
|
+
error('Unable to run %s' % ' '.join(diff_cmd))
|
|
510
|
+
|
|
511
|
+
# Report the number of objects found
|
|
512
|
+
|
|
513
|
+
if args.count_only:
|
|
514
|
+
print(len(files))
|
|
515
|
+
|
|
516
|
+
if args.count:
|
|
517
|
+
print()
|
|
518
|
+
print(f'{len(files)} objects found')
|
|
519
|
+
|
|
520
|
+
################################################################################
|
|
521
|
+
|
|
522
|
+
def ffind():
|
|
523
|
+
"""Entry point"""
|
|
524
|
+
|
|
525
|
+
try:
|
|
526
|
+
main()
|
|
527
|
+
except KeyboardInterrupt:
|
|
528
|
+
sys.exit(1)
|
|
529
|
+
except BrokenPipeError:
|
|
530
|
+
sys.exit(2)
|
|
531
|
+
|
|
532
|
+
################################################################################
|
|
533
|
+
|
|
534
|
+
if __name__ == '__main__':
|
|
535
|
+
ffind()
|
skilleter_thingy/ggit.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
""" Run a git command in all working trees in/under the specified subdirectory """
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
import argparse
|
|
9
|
+
|
|
10
|
+
from skilleter_modules import git
|
|
11
|
+
from skilleter_modules import colour
|
|
12
|
+
|
|
13
|
+
################################################################################
|
|
14
|
+
|
|
15
|
+
def run_git(args, directory, command):
|
|
16
|
+
""" Run a git command in the specified directory """
|
|
17
|
+
|
|
18
|
+
if not args.quiet:
|
|
19
|
+
colour.write('\n[BOLD][GREEN:Running git %s in %s][NORMAL]\n' % (' '.join(command), directory))
|
|
20
|
+
|
|
21
|
+
sys.stdout.flush()
|
|
22
|
+
|
|
23
|
+
subprocess.run(['git', '-C', directory] + command)
|
|
24
|
+
|
|
25
|
+
################################################################################
|
|
26
|
+
|
|
27
|
+
def parse_command_line():
|
|
28
|
+
""" Parse the command line options """
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
argpos = sys.argv.index('--')
|
|
32
|
+
|
|
33
|
+
cmd = sys.argv[argpos + 1:]
|
|
34
|
+
sys.argv = sys.argv[:argpos]
|
|
35
|
+
except ValueError:
|
|
36
|
+
cmd = sys.argv[1:]
|
|
37
|
+
sys.argv = sys.argv[:1]
|
|
38
|
+
|
|
39
|
+
parser = argparse.ArgumentParser(description='Run a git command recursively in subdirectories')
|
|
40
|
+
|
|
41
|
+
parser.add_argument('--quiet', '-q', action='store_true', help='Run quietly - only output errors or output from git')
|
|
42
|
+
parser.add_argument('path', nargs='?', action='store', default='.', help='Specify the path to run in')
|
|
43
|
+
|
|
44
|
+
args = parser.parse_args()
|
|
45
|
+
|
|
46
|
+
return args, cmd
|
|
47
|
+
|
|
48
|
+
################################################################################
|
|
49
|
+
|
|
50
|
+
def main():
|
|
51
|
+
""" Main function """
|
|
52
|
+
|
|
53
|
+
args, cmdline = parse_command_line()
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
os.chdir(args.path)
|
|
57
|
+
except FileNotFoundError:
|
|
58
|
+
colour.error(f'Invalid path [BLUE:{args.path}]', prefix=True)
|
|
59
|
+
|
|
60
|
+
# If the current directory is in a working tree and below the top-level
|
|
61
|
+
# directory then we run the git command here as well as in subdirectories
|
|
62
|
+
# that are working trees.
|
|
63
|
+
|
|
64
|
+
if git.working_tree() and not os.path.isdir('.git'):
|
|
65
|
+
run_git(args, os.getcwd(), cmdline)
|
|
66
|
+
|
|
67
|
+
# Find working trees and run the command in them
|
|
68
|
+
|
|
69
|
+
for root, dirs, _ in os.walk(args.path):
|
|
70
|
+
if '.git' in dirs:
|
|
71
|
+
run_git(args, root, cmdline)
|
|
72
|
+
|
|
73
|
+
################################################################################
|
|
74
|
+
|
|
75
|
+
def ggit():
|
|
76
|
+
"""Entry point"""
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
main()
|
|
80
|
+
except KeyboardInterrupt:
|
|
81
|
+
sys.exit(1)
|
|
82
|
+
except BrokenPipeError:
|
|
83
|
+
sys.exit(2)
|
|
84
|
+
|
|
85
|
+
################################################################################
|
|
86
|
+
|
|
87
|
+
if __name__ == '__main__':
|
|
88
|
+
ggit()
|