skilleter-thingy 0.0.22__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.

Potentially problematic release.


This version of skilleter-thingy might be problematic. Click here for more details.

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