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.
Files changed (39) hide show
  1. skilleter_thingy/__init__.py +0 -0
  2. skilleter_thingy/addpath.py +107 -0
  3. skilleter_thingy/console_colours.py +63 -0
  4. skilleter_thingy/ffind.py +535 -0
  5. skilleter_thingy/ggit.py +88 -0
  6. skilleter_thingy/ggrep.py +155 -0
  7. skilleter_thingy/git_br.py +186 -0
  8. skilleter_thingy/git_ca.py +147 -0
  9. skilleter_thingy/git_cleanup.py +297 -0
  10. skilleter_thingy/git_co.py +227 -0
  11. skilleter_thingy/git_common.py +68 -0
  12. skilleter_thingy/git_hold.py +162 -0
  13. skilleter_thingy/git_parent.py +84 -0
  14. skilleter_thingy/git_retag.py +67 -0
  15. skilleter_thingy/git_review.py +1450 -0
  16. skilleter_thingy/git_update.py +398 -0
  17. skilleter_thingy/git_wt.py +72 -0
  18. skilleter_thingy/gitcmp_helper.py +328 -0
  19. skilleter_thingy/gitprompt.py +293 -0
  20. skilleter_thingy/linecount.py +154 -0
  21. skilleter_thingy/multigit.py +915 -0
  22. skilleter_thingy/py_audit.py +133 -0
  23. skilleter_thingy/remdir.py +127 -0
  24. skilleter_thingy/rpylint.py +98 -0
  25. skilleter_thingy/strreplace.py +82 -0
  26. skilleter_thingy/test.py +34 -0
  27. skilleter_thingy/tfm.py +948 -0
  28. skilleter_thingy/tfparse.py +101 -0
  29. skilleter_thingy/trimpath.py +82 -0
  30. skilleter_thingy/venv_create.py +47 -0
  31. skilleter_thingy/venv_template.py +47 -0
  32. skilleter_thingy/xchmod.py +124 -0
  33. skilleter_thingy/yamlcheck.py +89 -0
  34. skilleter_thingy-0.3.14.dist-info/METADATA +606 -0
  35. skilleter_thingy-0.3.14.dist-info/RECORD +39 -0
  36. skilleter_thingy-0.3.14.dist-info/WHEEL +5 -0
  37. skilleter_thingy-0.3.14.dist-info/entry_points.txt +31 -0
  38. skilleter_thingy-0.3.14.dist-info/licenses/LICENSE +619 -0
  39. 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()
@@ -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()