skilleter-thingy 0.0.40__py3-none-any.whl → 0.0.42__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.
- skilleter_thingy/__init__.py +6 -0
- skilleter_thingy/addpath.py +107 -0
- skilleter_thingy/borger.py +269 -0
- skilleter_thingy/console_colours.py +63 -0
- skilleter_thingy/diskspacecheck.py +67 -0
- skilleter_thingy/docker_purge.py +113 -0
- skilleter_thingy/ffind.py +536 -0
- skilleter_thingy/ggit.py +90 -0
- skilleter_thingy/ggrep.py +154 -0
- skilleter_thingy/git_br.py +180 -0
- skilleter_thingy/git_ca.py +142 -0
- skilleter_thingy/git_cleanup.py +287 -0
- skilleter_thingy/git_co.py +220 -0
- skilleter_thingy/git_common.py +61 -0
- skilleter_thingy/git_hold.py +154 -0
- skilleter_thingy/git_mr.py +92 -0
- skilleter_thingy/git_parent.py +77 -0
- skilleter_thingy/git_review.py +1428 -0
- skilleter_thingy/git_update.py +385 -0
- skilleter_thingy/git_wt.py +96 -0
- skilleter_thingy/gitcmp_helper.py +322 -0
- skilleter_thingy/gitprompt.py +274 -0
- skilleter_thingy/gl.py +174 -0
- skilleter_thingy/gphotosync.py +610 -0
- skilleter_thingy/linecount.py +155 -0
- skilleter_thingy/moviemover.py +133 -0
- skilleter_thingy/photodupe.py +136 -0
- skilleter_thingy/phototidier.py +248 -0
- skilleter_thingy/py_audit.py +131 -0
- skilleter_thingy/readable.py +270 -0
- skilleter_thingy/remdir.py +126 -0
- skilleter_thingy/rmdupe.py +550 -0
- skilleter_thingy/rpylint.py +91 -0
- skilleter_thingy/splitpics.py +99 -0
- skilleter_thingy/strreplace.py +82 -0
- skilleter_thingy/sysmon.py +435 -0
- skilleter_thingy/tfm.py +920 -0
- skilleter_thingy/tfparse.py +101 -0
- skilleter_thingy/thingy/__init__.py +6 -0
- skilleter_thingy/thingy/colour.py +213 -0
- skilleter_thingy/thingy/dc_curses.py +278 -0
- skilleter_thingy/thingy/dc_defaults.py +221 -0
- skilleter_thingy/thingy/dc_util.py +50 -0
- skilleter_thingy/thingy/dircolors.py +308 -0
- skilleter_thingy/thingy/docker.py +95 -0
- skilleter_thingy/thingy/files.py +142 -0
- skilleter_thingy/thingy/git.py +1371 -0
- skilleter_thingy/thingy/git2.py +1307 -0
- skilleter_thingy/thingy/gitlab.py +193 -0
- skilleter_thingy/thingy/logger.py +112 -0
- skilleter_thingy/thingy/path.py +156 -0
- skilleter_thingy/thingy/popup.py +87 -0
- skilleter_thingy/thingy/process.py +112 -0
- skilleter_thingy/thingy/run.py +334 -0
- skilleter_thingy/thingy/tfm_pane.py +595 -0
- skilleter_thingy/thingy/tidy.py +160 -0
- skilleter_thingy/trimpath.py +84 -0
- skilleter_thingy/window_rename.py +92 -0
- skilleter_thingy/xchmod.py +125 -0
- skilleter_thingy/yamlcheck.py +89 -0
- {skilleter_thingy-0.0.40.dist-info → skilleter_thingy-0.0.42.dist-info}/METADATA +5 -1
- skilleter_thingy-0.0.42.dist-info/RECORD +66 -0
- {skilleter_thingy-0.0.40.dist-info → skilleter_thingy-0.0.42.dist-info}/entry_points.txt +1 -0
- skilleter_thingy-0.0.42.dist-info/top_level.txt +1 -0
- skilleter_thingy-0.0.40.dist-info/RECORD +0 -6
- skilleter_thingy-0.0.40.dist-info/top_level.txt +0 -1
- {skilleter_thingy-0.0.40.dist-info → skilleter_thingy-0.0.42.dist-info}/LICENSE +0 -0
- {skilleter_thingy-0.0.40.dist-info → skilleter_thingy-0.0.42.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
""" Query api.osv.dev to determine whether a specified version of a particular
|
|
4
|
+
Python package is subject to known security vulnerabilities """
|
|
5
|
+
|
|
6
|
+
################################################################################
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import requests
|
|
11
|
+
import subprocess
|
|
12
|
+
import tempfile
|
|
13
|
+
import re
|
|
14
|
+
import glob
|
|
15
|
+
import argparse
|
|
16
|
+
|
|
17
|
+
################################################################################
|
|
18
|
+
|
|
19
|
+
PIP_PACKAGES = ('pip', 'pkg_resources', 'setuptools')
|
|
20
|
+
PIP_OPTIONS = '--no-cache-dir'
|
|
21
|
+
|
|
22
|
+
QUERY_URL = "https://api.osv.dev/v1/query"
|
|
23
|
+
QUERY_HEADERS = {'content-type': 'application/json', 'Accept-Charset': 'UTF-8'}
|
|
24
|
+
|
|
25
|
+
ANSI_NORMAL = '\x1b[0m'
|
|
26
|
+
ANSI_BOLD = '\x1b[1m'
|
|
27
|
+
|
|
28
|
+
################################################################################
|
|
29
|
+
|
|
30
|
+
def audit(package, version):
|
|
31
|
+
""" Main function """
|
|
32
|
+
|
|
33
|
+
# Query api.osv.dev for known vulnerabilties in this version of the package
|
|
34
|
+
|
|
35
|
+
payload = '{"version": "'+version+'", "package": {"name": "'+package+'", "ecosystem": "PyPI"}}'
|
|
36
|
+
result = requests.post(QUERY_URL, data=payload, headers=QUERY_HEADERS)
|
|
37
|
+
|
|
38
|
+
# Parse and report the results
|
|
39
|
+
|
|
40
|
+
details = result.json()
|
|
41
|
+
|
|
42
|
+
print('-' * 80)
|
|
43
|
+
if package in PIP_PACKAGES:
|
|
44
|
+
print(f'Package: {package} {version} (part of Pip)')
|
|
45
|
+
else:
|
|
46
|
+
print(f'Package: {package} {version}')
|
|
47
|
+
|
|
48
|
+
print()
|
|
49
|
+
|
|
50
|
+
if 'vulns' in details:
|
|
51
|
+
print(f'{len(details["vulns"])} known vulnerabilities')
|
|
52
|
+
|
|
53
|
+
for v in details['vulns']:
|
|
54
|
+
print()
|
|
55
|
+
print(f'{ANSI_BOLD}Vulnerability: {v["id"]}{ANSI_NORMAL}')
|
|
56
|
+
|
|
57
|
+
if 'summary' in v:
|
|
58
|
+
print(f'Summary: {v["summary"]}')
|
|
59
|
+
|
|
60
|
+
if 'aliases' in v:
|
|
61
|
+
print('Aliases: %s' % (', '.join(v['aliases'])))
|
|
62
|
+
|
|
63
|
+
if 'details' in v:
|
|
64
|
+
print()
|
|
65
|
+
print(v['details'])
|
|
66
|
+
else:
|
|
67
|
+
print(f'No known vulnerabilities')
|
|
68
|
+
|
|
69
|
+
################################################################################
|
|
70
|
+
|
|
71
|
+
def main():
|
|
72
|
+
""" Entry point """
|
|
73
|
+
|
|
74
|
+
parser = argparse.ArgumentParser(description='Query api.osv.dev to determine whether Python packagers in a requirments.txt file are subject to known security vulnerabilities')
|
|
75
|
+
parser.add_argument('requirements', nargs='*', type=str, action='store', help='The requirements file (if not specified, then the script searches for a requirements.txt file)')
|
|
76
|
+
args = parser.parse_args()
|
|
77
|
+
|
|
78
|
+
requirements = args.requirements or glob.glob('**/requirements.txt', recursive=True)
|
|
79
|
+
|
|
80
|
+
if not requirements:
|
|
81
|
+
print('No requirements.txt file(s) found')
|
|
82
|
+
sys.exit(0)
|
|
83
|
+
|
|
84
|
+
# Create a venv for each requirements file, install pip and the packages
|
|
85
|
+
# and prerequisites, get the list of installed package versions
|
|
86
|
+
# and check each one.
|
|
87
|
+
|
|
88
|
+
for requirement in requirements:
|
|
89
|
+
print('=' * 80)
|
|
90
|
+
print(f'{ANSI_BOLD}File: {requirement}{ANSI_NORMAL}')
|
|
91
|
+
print()
|
|
92
|
+
|
|
93
|
+
with tempfile.TemporaryDirectory() as env_dir:
|
|
94
|
+
with tempfile.NamedTemporaryFile() as package_list:
|
|
95
|
+
script = f'python3 -m venv {env_dir}' \
|
|
96
|
+
f' && . {env_dir}/bin/activate' \
|
|
97
|
+
f' && python3 -m pip {PIP_OPTIONS} install --upgrade pip setuptools' \
|
|
98
|
+
f' && python3 -m pip {PIP_OPTIONS} install -r {requirement}' \
|
|
99
|
+
f' && python3 -m pip {PIP_OPTIONS} list | tail -n+3 | tee {package_list.name}' \
|
|
100
|
+
' && deactivate'
|
|
101
|
+
|
|
102
|
+
result = subprocess.run(script, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True)
|
|
103
|
+
|
|
104
|
+
if result.returncode:
|
|
105
|
+
print(f'ERROR #{result.returncode}: {result.stdout}')
|
|
106
|
+
sys.exit(result.returncode)
|
|
107
|
+
|
|
108
|
+
with open(package_list.name) as infile:
|
|
109
|
+
for package in infile.readlines():
|
|
110
|
+
package_info = re.split(' +', package.strip())
|
|
111
|
+
|
|
112
|
+
audit(package_info[0], package_info[1])
|
|
113
|
+
|
|
114
|
+
################################################################################
|
|
115
|
+
|
|
116
|
+
def py_audit():
|
|
117
|
+
"""Entry point"""
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
main()
|
|
121
|
+
|
|
122
|
+
except KeyboardInterrupt:
|
|
123
|
+
sys.exit(1)
|
|
124
|
+
|
|
125
|
+
except BrokenPipeError:
|
|
126
|
+
sys.exit(2)
|
|
127
|
+
|
|
128
|
+
################################################################################
|
|
129
|
+
|
|
130
|
+
if __name__ == '__main__':
|
|
131
|
+
py_audit()
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
#! /usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
################################################################################
|
|
4
|
+
""" Pipe for converting colour combinations to make them readable
|
|
5
|
+
|
|
6
|
+
Defaults to making things readable on a light background but has the
|
|
7
|
+
option to make things readable on a dark background or for removing
|
|
8
|
+
colours altogether
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
# TODO: Not sure it works properly as a pipe
|
|
12
|
+
# TODO: Error handling in file I/O
|
|
13
|
+
|
|
14
|
+
################################################################################
|
|
15
|
+
|
|
16
|
+
import sys
|
|
17
|
+
import argparse
|
|
18
|
+
import tempfile
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
import filecmp
|
|
22
|
+
import shutil
|
|
23
|
+
|
|
24
|
+
import thingy.tidy as tidy
|
|
25
|
+
import thingy.files as files
|
|
26
|
+
|
|
27
|
+
################################################################################
|
|
28
|
+
|
|
29
|
+
TF_OBJECTS_CHANGED_END = 'Terraform detected the following changes made outside of Terraform since the last "terraform apply":'
|
|
30
|
+
TF_IGNORE_MSG = 'Unless you have made equivalent changes to your configuration, or ignored the relevant attributes using ignore_changes, the following plan may include actions to undo or respond to these changes.'
|
|
31
|
+
|
|
32
|
+
TF_REFRESHING_AND_READING = re.compile(r'.*: (?:Refreshing state\.\.\.|Reading\.\.\.|Read complete after |Preparing import\.\.\.).*')
|
|
33
|
+
TF_FINDING_AND_INSTALLING = re.compile(r'- (?:Finding .*[.]{3}|Installing .*[.]{3}|Installed .*)')
|
|
34
|
+
|
|
35
|
+
TF_HAS_CHANGED = re.compile(r' # .* has changed')
|
|
36
|
+
TF_READ_DURING_APPLY = re.compile(r' +# [-a-z_.0-9\[\]]+ will be read during apply')
|
|
37
|
+
TF_UNCHANGED_TAG = re.compile(r' +".*" += +".*"')
|
|
38
|
+
|
|
39
|
+
TF_OBJECTS_CHANGED = 'Note: Objects have changed outside of Terraform'
|
|
40
|
+
TF_UNCHANGED_HIDDEN = re.compile(r' +# \(\d+ unchanged (?:attribute|block|element)s? hidden\)')
|
|
41
|
+
|
|
42
|
+
################################################################################
|
|
43
|
+
|
|
44
|
+
def parse_command_line():
|
|
45
|
+
""" Parse, check and sanitise the command line arguments """
|
|
46
|
+
|
|
47
|
+
parser = argparse.ArgumentParser(description='Read from standard input and write to standard output modifying ANSI colour codes en-route.')
|
|
48
|
+
|
|
49
|
+
parser.add_argument('-l', '--light', action='store_true', help='Modify colours for a light background (the default)')
|
|
50
|
+
parser.add_argument('-d', '--dark', action='store_true', help='Modify colours for a dark background')
|
|
51
|
+
parser.add_argument('-n', '--none', action='store_true', help='Remove all colour codes')
|
|
52
|
+
parser.add_argument('-t', '--tidy', action='store_true',
|
|
53
|
+
help='Remove colour codes and stuff that typically occurs in log files causing diffs, but is of no particular interest (e.g. SHA1 values, times, dates)')
|
|
54
|
+
parser.add_argument('-s', '--strip-blank', action='store_true', help='Strip all blank lines')
|
|
55
|
+
parser.add_argument('-D', '--debug', action='store_true', help='Replace colours with debug information')
|
|
56
|
+
parser.add_argument('-o', '--out', action='store_true', help='Output to standard output rather than overwriting input files')
|
|
57
|
+
parser.add_argument('-O', '--dir', action='store', default=None, help='Store output files in the specified directory (creating it if it doesn\'t exist)')
|
|
58
|
+
parser.add_argument('-a', '--aws', action='store_true', help='Remove AWS resource IDs')
|
|
59
|
+
parser.add_argument('-T', '--terraform', action='store_true', help='Clean Terraform plan/apply log files')
|
|
60
|
+
parser.add_argument('files', nargs='*', default=None, help='The files to convert (use stdin/stout if no input files are specified)')
|
|
61
|
+
|
|
62
|
+
args = parser.parse_args()
|
|
63
|
+
|
|
64
|
+
# Can't do more than one transformation
|
|
65
|
+
|
|
66
|
+
if args.light + args.dark + args.none + args.debug > 1:
|
|
67
|
+
print('ERROR: Only one colour conversion option can be specified')
|
|
68
|
+
sys.exit(1)
|
|
69
|
+
|
|
70
|
+
if args.tidy and (args.light or args.dark or args.debug):
|
|
71
|
+
print('ERROR: The tidy and colour conversion options cannot be specified together')
|
|
72
|
+
sys.exit(1)
|
|
73
|
+
|
|
74
|
+
# Default to doing cleanup - removing colour codes, times,
|
|
75
|
+
|
|
76
|
+
if not args.light and not args.dark and not args.tidy and not args.strip_blank and not args.terraform:
|
|
77
|
+
args.strip_blank = args.none = args.tidy = args.terraform = True
|
|
78
|
+
|
|
79
|
+
# Terraform option also removes ANSI, etc.
|
|
80
|
+
|
|
81
|
+
if args.terraform:
|
|
82
|
+
args.strip_blank = args.tidy = args.none = True
|
|
83
|
+
|
|
84
|
+
# Create the output directory, if required
|
|
85
|
+
|
|
86
|
+
if args.dir and not os.path.isdir(args.dir):
|
|
87
|
+
os.mkdir(args.dir)
|
|
88
|
+
|
|
89
|
+
return args
|
|
90
|
+
|
|
91
|
+
################################################################################
|
|
92
|
+
|
|
93
|
+
def cleanfile(args, infile, outfile):
|
|
94
|
+
""" Clean infile, writing to outfile.
|
|
95
|
+
Returns True if any changes were made. """
|
|
96
|
+
|
|
97
|
+
# Set if we are ignoring a block of adata
|
|
98
|
+
|
|
99
|
+
ignore_until = False
|
|
100
|
+
|
|
101
|
+
prev_line = None
|
|
102
|
+
|
|
103
|
+
ignore_until = None
|
|
104
|
+
collection = []
|
|
105
|
+
|
|
106
|
+
# Read, process and write stdin to stdout, converting appropriately
|
|
107
|
+
|
|
108
|
+
for data in infile:
|
|
109
|
+
# Remove the trailing newline
|
|
110
|
+
|
|
111
|
+
if data[-1] == '\n':
|
|
112
|
+
data = data[:-1]
|
|
113
|
+
|
|
114
|
+
# Strip trailing whitespace
|
|
115
|
+
|
|
116
|
+
data = data.rstrip()
|
|
117
|
+
|
|
118
|
+
# Do colour code conversion
|
|
119
|
+
|
|
120
|
+
if args.debug:
|
|
121
|
+
clean = tidy.debug_format(data)
|
|
122
|
+
elif args.light:
|
|
123
|
+
clean = tidy.convert_ansi(data, True)
|
|
124
|
+
elif args.dark:
|
|
125
|
+
clean = tidy.convert_ansi(data, False)
|
|
126
|
+
elif args.none:
|
|
127
|
+
clean = tidy.remove_ansi(data)
|
|
128
|
+
else:
|
|
129
|
+
clean = data
|
|
130
|
+
|
|
131
|
+
# Do tidying
|
|
132
|
+
|
|
133
|
+
if args.tidy:
|
|
134
|
+
clean = tidy.remove_sha256(clean)
|
|
135
|
+
clean = tidy.remove_sha1(clean)
|
|
136
|
+
clean = tidy.remove_times(clean)
|
|
137
|
+
|
|
138
|
+
if not args.light and not args.dark:
|
|
139
|
+
clean = tidy.remove_ansi(clean)
|
|
140
|
+
|
|
141
|
+
# Remove AWS ID values
|
|
142
|
+
|
|
143
|
+
if args.aws:
|
|
144
|
+
clean = tidy.remove_aws_ids(clean)
|
|
145
|
+
|
|
146
|
+
# Do things with Terraform log data
|
|
147
|
+
|
|
148
|
+
if args.terraform:
|
|
149
|
+
if TF_HAS_CHANGED.fullmatch(clean):
|
|
150
|
+
ignore_until = ' }'
|
|
151
|
+
|
|
152
|
+
elif TF_READ_DURING_APPLY.fullmatch(clean):
|
|
153
|
+
ignore_until = ' }'
|
|
154
|
+
|
|
155
|
+
elif clean == TF_OBJECTS_CHANGED:
|
|
156
|
+
ignore_until = TF_OBJECTS_CHANGED_END
|
|
157
|
+
|
|
158
|
+
elif TF_UNCHANGED_HIDDEN.fullmatch(clean):
|
|
159
|
+
clean = None
|
|
160
|
+
|
|
161
|
+
elif clean == TF_IGNORE_MSG:
|
|
162
|
+
clean = None
|
|
163
|
+
|
|
164
|
+
elif TF_UNCHANGED_TAG.fullmatch(clean):
|
|
165
|
+
clean = None
|
|
166
|
+
|
|
167
|
+
elif TF_REFRESHING_AND_READING.match(clean) or TF_FINDING_AND_INSTALLING.fullmatch(clean):
|
|
168
|
+
# Collect a block of non-deterministically-ordered data
|
|
169
|
+
|
|
170
|
+
collection.append(clean)
|
|
171
|
+
clean = None
|
|
172
|
+
|
|
173
|
+
elif collection:
|
|
174
|
+
# If we collected a block, write it out in sorted order
|
|
175
|
+
|
|
176
|
+
collection.sort()
|
|
177
|
+
for entry in collection:
|
|
178
|
+
outfile.write(entry)
|
|
179
|
+
outfile.write('\n')
|
|
180
|
+
|
|
181
|
+
collection = []
|
|
182
|
+
|
|
183
|
+
# Write normal output, skipping >1 blank lines
|
|
184
|
+
|
|
185
|
+
if clean is not None and not ignore_until:
|
|
186
|
+
if clean != '' or prev_line != '':
|
|
187
|
+
outfile.write(clean)
|
|
188
|
+
outfile.write('\n')
|
|
189
|
+
|
|
190
|
+
prev_line = clean
|
|
191
|
+
|
|
192
|
+
# Clear the ignore flag if we've hid the end marker of the block
|
|
193
|
+
|
|
194
|
+
if ignore_until and clean == ignore_until:
|
|
195
|
+
ignore_until = None
|
|
196
|
+
|
|
197
|
+
################################################################################
|
|
198
|
+
|
|
199
|
+
def main():
|
|
200
|
+
""" Main code """
|
|
201
|
+
|
|
202
|
+
# Process command line options
|
|
203
|
+
|
|
204
|
+
args = parse_command_line()
|
|
205
|
+
|
|
206
|
+
# We are either processing 1 or more files, or just piping stdin to stdout
|
|
207
|
+
|
|
208
|
+
if args.files:
|
|
209
|
+
try:
|
|
210
|
+
for filename in args.files:
|
|
211
|
+
with open(filename, 'rt') as infile:
|
|
212
|
+
|
|
213
|
+
# Either write to stdout or to a temporary file
|
|
214
|
+
|
|
215
|
+
if args.out:
|
|
216
|
+
outfile = sys.stdout
|
|
217
|
+
outfile_name = None
|
|
218
|
+
else:
|
|
219
|
+
outfile = tempfile.NamedTemporaryFile(mode='wt+', delete=False)
|
|
220
|
+
outfile_name = outfile.name
|
|
221
|
+
|
|
222
|
+
cleanfile(args, infile, outfile)
|
|
223
|
+
|
|
224
|
+
if outfile_name:
|
|
225
|
+
outfile.close()
|
|
226
|
+
|
|
227
|
+
# If we wrote to an output file then do something
|
|
228
|
+
|
|
229
|
+
if outfile_name:
|
|
230
|
+
if args.dir:
|
|
231
|
+
# Writing to a directory - just move the output file there unconditionally
|
|
232
|
+
|
|
233
|
+
shutil.move(outfile_name, os.path.join(args.dir, os.path.basename(filename)))
|
|
234
|
+
|
|
235
|
+
elif not filecmp.cmp(outfile_name, filename, shallow=False):
|
|
236
|
+
# Only backup and write the source file if changes have been made (original is
|
|
237
|
+
# left in place without a backup if nothing's changed)
|
|
238
|
+
|
|
239
|
+
files.backup(filename)
|
|
240
|
+
|
|
241
|
+
shutil.move(outfile_name, filename)
|
|
242
|
+
|
|
243
|
+
else:
|
|
244
|
+
# Output file hasn't been used, so just delete it
|
|
245
|
+
|
|
246
|
+
os.unlink(outfile_name)
|
|
247
|
+
|
|
248
|
+
except FileNotFoundError as exc:
|
|
249
|
+
print(exc)
|
|
250
|
+
sys.exit(1)
|
|
251
|
+
|
|
252
|
+
else:
|
|
253
|
+
cleanfile(args, sys.stdin, sys.stdout)
|
|
254
|
+
|
|
255
|
+
################################################################################
|
|
256
|
+
|
|
257
|
+
def readable():
|
|
258
|
+
"""Entry point"""
|
|
259
|
+
|
|
260
|
+
try:
|
|
261
|
+
main()
|
|
262
|
+
except KeyboardInterrupt:
|
|
263
|
+
sys.exit(1)
|
|
264
|
+
except BrokenPipeError:
|
|
265
|
+
sys.exit(2)
|
|
266
|
+
|
|
267
|
+
################################################################################
|
|
268
|
+
|
|
269
|
+
if __name__ == '__main__':
|
|
270
|
+
readable()
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#! /usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
################################################################################
|
|
4
|
+
""" remdir - remove empty directories
|
|
5
|
+
|
|
6
|
+
Given the name of a directory tree and, optionally, a list of files to ignore,
|
|
7
|
+
recursively deletes any directory in the tree that is either completely empty
|
|
8
|
+
or contains nothing but files matching the list.
|
|
9
|
+
|
|
10
|
+
For example:
|
|
11
|
+
|
|
12
|
+
remdir /backup --ignore '*.bak' --keep .stfolder
|
|
13
|
+
|
|
14
|
+
would remove any directory within the /backup tree that is empty
|
|
15
|
+
or contains only files that match the '*.bak' wildcard so long as
|
|
16
|
+
it isn't called '.stfolder'.
|
|
17
|
+
|
|
18
|
+
TODO: Using os.walk() means that, if a directory is deleted, it still shows
|
|
19
|
+
up in the list of directories in the parent directory (?)
|
|
20
|
+
"""
|
|
21
|
+
################################################################################
|
|
22
|
+
|
|
23
|
+
import sys
|
|
24
|
+
import os
|
|
25
|
+
import argparse
|
|
26
|
+
import fnmatch
|
|
27
|
+
import shutil
|
|
28
|
+
|
|
29
|
+
import thingy.colour as colour
|
|
30
|
+
import thingy.logger as logger
|
|
31
|
+
|
|
32
|
+
log = logger.init('remdir')
|
|
33
|
+
|
|
34
|
+
################################################################################
|
|
35
|
+
|
|
36
|
+
def main():
|
|
37
|
+
""" Entry point """
|
|
38
|
+
|
|
39
|
+
# Parse the command line
|
|
40
|
+
|
|
41
|
+
parser = argparse.ArgumentParser(description='Remove empty directories')
|
|
42
|
+
parser.add_argument('-D', '--dry-run', action='store_true', help='Dry-run - report what would be done without doing anything')
|
|
43
|
+
parser.add_argument('--debug', action='store_true', help='Output debug information')
|
|
44
|
+
parser.add_argument('--verbose', action='store_true', help='Output verbose information')
|
|
45
|
+
parser.add_argument('-I', '--ignore', action='append', help='Files to ignore when considering whether a directory is empty')
|
|
46
|
+
parser.add_argument('-K', '--keep', action='append', help='Directories that should be kept even if they are empty')
|
|
47
|
+
parser.add_argument('dirs', nargs='+', help='Directories to prune')
|
|
48
|
+
args = parser.parse_args()
|
|
49
|
+
|
|
50
|
+
if args.debug:
|
|
51
|
+
log.setLevel(logger.DEBUG)
|
|
52
|
+
|
|
53
|
+
# Go through each directory
|
|
54
|
+
|
|
55
|
+
for directory in args.dirs:
|
|
56
|
+
if not os.path.isdir(directory):
|
|
57
|
+
colour.write(f'"{directory}" is not a directory')
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
# Walk through the directory tree in bottom-up order
|
|
61
|
+
|
|
62
|
+
for root, dirs, files in os.walk(directory, topdown=False):
|
|
63
|
+
log.debug('')
|
|
64
|
+
log.debug('Directory: %s', root)
|
|
65
|
+
log.debug(' Sub-directories : %s', dirs)
|
|
66
|
+
log.debug(' Files : %s', files)
|
|
67
|
+
|
|
68
|
+
# Only consider directories with no subdirectories
|
|
69
|
+
|
|
70
|
+
if dirs:
|
|
71
|
+
log.debug('Ignoring directory "%s" as it has %d subdirectories', root, len(dirs))
|
|
72
|
+
else:
|
|
73
|
+
# Count of files (if any) to preserve in the directory
|
|
74
|
+
|
|
75
|
+
filecount = len(files)
|
|
76
|
+
|
|
77
|
+
# If any file matches an entry in the ignore list (if we have one) then decrement the file count
|
|
78
|
+
|
|
79
|
+
if args.ignore:
|
|
80
|
+
for file in files:
|
|
81
|
+
for ignore in args.ignore:
|
|
82
|
+
if fnmatch.fnmatch(file, ignore):
|
|
83
|
+
filecount -= 1
|
|
84
|
+
break
|
|
85
|
+
|
|
86
|
+
# If no non-matching files then delete the directory unless it is in the keep list
|
|
87
|
+
|
|
88
|
+
if filecount == 0:
|
|
89
|
+
keep_dir = False
|
|
90
|
+
for keep in args.keep:
|
|
91
|
+
if fnmatch.fnmatch(os.path.basename(root), keep):
|
|
92
|
+
keep_dir = True
|
|
93
|
+
break
|
|
94
|
+
|
|
95
|
+
if keep_dir:
|
|
96
|
+
colour.write(f'Keeping empty directory [BLUE:{root}]')
|
|
97
|
+
else:
|
|
98
|
+
log.debug('Deleting directory: %s', root)
|
|
99
|
+
colour.write(f'Deleting "[BLUE:{root}]"')
|
|
100
|
+
|
|
101
|
+
if not args.dry_run:
|
|
102
|
+
# Delete the directory and contents
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
shutil.rmtree(root)
|
|
106
|
+
except OSError:
|
|
107
|
+
colour.error('Unable to delete "[BLUE:{root}]"')
|
|
108
|
+
else:
|
|
109
|
+
log.debug('Ignoring directory "%s" as it has %d non-ignorable files', root, filecount)
|
|
110
|
+
|
|
111
|
+
################################################################################
|
|
112
|
+
|
|
113
|
+
def remdir():
|
|
114
|
+
"""Entry point"""
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
main()
|
|
118
|
+
except KeyboardInterrupt:
|
|
119
|
+
sys.exit(1)
|
|
120
|
+
except BrokenPipeError:
|
|
121
|
+
sys.exit(2)
|
|
122
|
+
|
|
123
|
+
################################################################################
|
|
124
|
+
|
|
125
|
+
if __name__ == '__main__':
|
|
126
|
+
remdir()
|