offgrep 0.3.0__tar.gz → 0.3.1__tar.gz
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.
- {offgrep-0.3.0 → offgrep-0.3.1}/PKG-INFO +1 -1
- {offgrep-0.3.0 → offgrep-0.3.1}/pyproject.toml +1 -1
- {offgrep-0.3.0 → offgrep-0.3.1}/src/offgrep/__main__.py +2 -3
- {offgrep-0.3.0 → offgrep-0.3.1}/src/offgrep/arguments.py +172 -105
- {offgrep-0.3.0 → offgrep-0.3.1}/src/offgrep/filesystem.py +18 -10
- offgrep-0.3.1/src/offgrep/output.py +185 -0
- {offgrep-0.3.0 → offgrep-0.3.1}/src/offgrep/pattern_matching.py +15 -107
- {offgrep-0.3.0 → offgrep-0.3.1}/LICENSE +0 -0
- {offgrep-0.3.0 → offgrep-0.3.1}/README.md +0 -0
- {offgrep-0.3.0 → offgrep-0.3.1}/src/offgrep/__init__.py +0 -0
- {offgrep-0.3.0 → offgrep-0.3.1}/src/offgrep/commons.py +0 -0
- {offgrep-0.3.0 → offgrep-0.3.1}/src/offgrep/git.py +0 -0
- {offgrep-0.3.0 → offgrep-0.3.1}/src/offgrep/path_matching.py +0 -0
|
@@ -8,7 +8,8 @@ import sys
|
|
|
8
8
|
|
|
9
9
|
from .arguments import parse_arguments
|
|
10
10
|
from .filesystem import FileSelectionOptions, iter_selected_files
|
|
11
|
-
from .
|
|
11
|
+
from .output import OutputOptions
|
|
12
|
+
from .pattern_matching import PatternOptions, find_in_files
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
def grep_files(
|
|
@@ -46,5 +47,3 @@ def main() -> int:
|
|
|
46
47
|
|
|
47
48
|
if __name__ == "__main__":
|
|
48
49
|
sys.exit(main()) # pragma: no cover
|
|
49
|
-
|
|
50
|
-
# vim: fileencoding=utf-8 ts=4 sts=4 sw=4 autoindent expandtab syntax=python:
|
|
@@ -8,20 +8,33 @@ from argparse import (
|
|
|
8
8
|
ArgumentParser,
|
|
9
9
|
Namespace,
|
|
10
10
|
RawDescriptionHelpFormatter,
|
|
11
|
-
REMAINDER,
|
|
12
11
|
SUPPRESS,
|
|
13
12
|
)
|
|
14
13
|
from importlib.metadata import PackageNotFoundError, version as metadata_version
|
|
15
|
-
from logging import DEBUG, WARNING, basicConfig, debug, warning
|
|
14
|
+
from logging import CRITICAL, DEBUG, INFO, WARNING, basicConfig, debug, warning
|
|
16
15
|
from pathlib import Path
|
|
17
16
|
from sys import stdin
|
|
18
17
|
from tomllib import loads as tomli_loads
|
|
19
18
|
|
|
20
19
|
from .commons import EMPTY, UTF_8, split_without_empty_strings
|
|
21
|
-
from .filesystem import
|
|
22
|
-
|
|
20
|
+
from .filesystem import (
|
|
21
|
+
FileSelectionOptions,
|
|
22
|
+
IgnoreFileMode,
|
|
23
|
+
Priority,
|
|
24
|
+
VCSDirectory,
|
|
25
|
+
is_an_eligible_path,
|
|
26
|
+
)
|
|
27
|
+
from .output import (
|
|
28
|
+
OutputOptions,
|
|
29
|
+
HIGHLIGHTERS,
|
|
30
|
+
SUPPORTED_COLORS,
|
|
31
|
+
print_highlighter_examples,
|
|
32
|
+
)
|
|
33
|
+
from .pattern_matching import PatternOptions
|
|
34
|
+
|
|
23
35
|
|
|
24
36
|
DEFAULT_PRE_IGNORE = ",".join(f"{dirname}/" for dirname in VCSDirectory)
|
|
37
|
+
DEFAULT_HIGHLIGHTER_KEY = "u"
|
|
25
38
|
|
|
26
39
|
PACKAGE_NAME = "offgrep"
|
|
27
40
|
"""The package name for metadata inspection"""
|
|
@@ -106,62 +119,120 @@ def print_supported_file_tyoes() -> None:
|
|
|
106
119
|
warning("--type-list option not implemented yet")
|
|
107
120
|
|
|
108
121
|
|
|
109
|
-
def
|
|
110
|
-
"""
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
122
|
+
def _add_output_options(parser: ArgumentParser) -> None:
|
|
123
|
+
"""Add output options to a parser instance"""
|
|
124
|
+
output_group = parser.add_argument_group("Output options")
|
|
125
|
+
loglevel_mutex = output_group.add_mutually_exclusive_group()
|
|
126
|
+
loglevel_mutex.add_argument(
|
|
127
|
+
"--verbose",
|
|
128
|
+
action="store_true",
|
|
129
|
+
help="log with level INFO",
|
|
130
|
+
)
|
|
131
|
+
loglevel_mutex.add_argument(
|
|
132
|
+
"--debug",
|
|
133
|
+
action="store_true",
|
|
134
|
+
help="log with level DEBUG",
|
|
135
|
+
)
|
|
136
|
+
loglevel_mutex.add_argument(
|
|
137
|
+
"-q",
|
|
138
|
+
"--quiet",
|
|
139
|
+
action="store_true",
|
|
140
|
+
help="no output except for CRITICAL errors",
|
|
141
|
+
)
|
|
142
|
+
match_highlight_mutex = output_group.add_mutually_exclusive_group()
|
|
143
|
+
match_highlight_mutex.add_argument(
|
|
144
|
+
"--hi",
|
|
145
|
+
"--highlight-matches",
|
|
146
|
+
nargs="?",
|
|
147
|
+
choices=sorted(HIGHLIGHTERS),
|
|
148
|
+
const=DEFAULT_HIGHLIGHTER_KEY,
|
|
149
|
+
help="Highlight matches with the provided profile."
|
|
150
|
+
" Default ist %(default)r (no highlighting) if not specified,"
|
|
151
|
+
" or %(const)r if provided without a value."
|
|
152
|
+
" Use the --highlighters option for an overview with examples.",
|
|
153
|
+
)
|
|
154
|
+
for color in SUPPORTED_COLORS:
|
|
155
|
+
match_highlight_mutex.add_argument(
|
|
156
|
+
f"--{color}",
|
|
157
|
+
action="store_const",
|
|
158
|
+
dest="hi",
|
|
159
|
+
const=color[0],
|
|
160
|
+
help=f"Highlight matches in {color}.",
|
|
161
|
+
)
|
|
115
162
|
#
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
formatter_class=RawDescriptionHelpFormatter,
|
|
163
|
+
match_highlight_mutex.add_argument(
|
|
164
|
+
"--highlighters",
|
|
165
|
+
action="store_true",
|
|
166
|
+
help="Show highlighter effects on examples.",
|
|
121
167
|
)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
post_ignore=EMPTY,
|
|
168
|
+
output_group.add_argument(
|
|
169
|
+
"--stats",
|
|
170
|
+
action="store_true",
|
|
171
|
+
help="Collect and show statistics.",
|
|
127
172
|
)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
"
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
dest="loglevel",
|
|
134
|
-
help="log with debug level",
|
|
173
|
+
output_group.add_argument(
|
|
174
|
+
"--files",
|
|
175
|
+
action="store_true",
|
|
176
|
+
help="Print the path of each file that would be searched,"
|
|
177
|
+
" without actually performing the search.",
|
|
135
178
|
)
|
|
136
|
-
|
|
179
|
+
output_group.add_argument(
|
|
180
|
+
"--type-list",
|
|
181
|
+
action="store_true",
|
|
182
|
+
help="NOT IMPLEMTED YET: print the list of supported file types.",
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _add_file_options(parser: ArgumentParser) -> None:
|
|
187
|
+
"""Add file options to a parser instance"""
|
|
188
|
+
file_group = parser.add_argument_group("File selection options")
|
|
189
|
+
file_group.add_argument(
|
|
137
190
|
"--default-ignore-dirs",
|
|
138
|
-
|
|
139
|
-
"
|
|
191
|
+
metavar="DIRECTORY-NAMES",
|
|
192
|
+
help="DEPRECATED, please use --pre-ignore or --post-ignore instead."
|
|
193
|
+
" Original purpose: always ignore the directories named here,"
|
|
194
|
+
" separated by commas.",
|
|
140
195
|
)
|
|
141
|
-
|
|
196
|
+
file_group.add_argument(
|
|
142
197
|
"--default-ignore-priority",
|
|
143
198
|
choices=list(Priority),
|
|
144
199
|
type=Priority,
|
|
145
|
-
help="
|
|
200
|
+
help="DEPRECATED, please use --pre-ignore or --post-ignore instead."
|
|
201
|
+
" Original purpose: priority for the ignore rules specified through"
|
|
202
|
+
" --default-ignore-dirs."
|
|
146
203
|
f" Choices: %(choices)s ({str(Priority.LOW)!r} is applied first but might"
|
|
147
204
|
" be overridden by eg. directory defined rules,"
|
|
148
205
|
f" while {str(Priority.HIGH)!r} is applied last but cannot be overridden anymore)."
|
|
149
|
-
f" The preset is {str(Priority.LOW)!r}."
|
|
150
|
-
" DEPRECATED, please use --pre-ignore or --post-ignore instead.",
|
|
206
|
+
f" The preset is {str(Priority.LOW)!r}.",
|
|
151
207
|
)
|
|
152
|
-
|
|
208
|
+
file_group.add_argument(
|
|
153
209
|
"--pre-ignore",
|
|
210
|
+
metavar="PATTERNS",
|
|
154
211
|
help="gitignore-style patterns for files and/or directories ignored"
|
|
155
212
|
" before applying found ignore file rules, joined with commas (,)."
|
|
156
|
-
" Defaults to VCS repository directories (%(default)
|
|
213
|
+
" Defaults to VCS repository directories (%(default)r).",
|
|
157
214
|
)
|
|
158
|
-
|
|
215
|
+
file_group.add_argument(
|
|
159
216
|
"--post-ignore",
|
|
217
|
+
metavar="PATTERNS",
|
|
160
218
|
help="gitignore-style patterns for files and/or directories ignored"
|
|
161
219
|
" after applying found ignore file rules, joined with commas (,)."
|
|
162
|
-
" Empty by default (%(default)
|
|
220
|
+
" Empty by default (%(default)r).",
|
|
163
221
|
)
|
|
164
|
-
|
|
222
|
+
file_group.add_argument(
|
|
223
|
+
"-t",
|
|
224
|
+
"--type",
|
|
225
|
+
nargs="+",
|
|
226
|
+
metavar="FILETYPE",
|
|
227
|
+
help="NOT IMPLEMTED YET: search only in files of type %(metavar)s.",
|
|
228
|
+
# See --type-list output for supported file types.",
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _add_pattern_options(parser: ArgumentParser) -> None:
|
|
233
|
+
"""Add pattern options to a parser instance"""
|
|
234
|
+
pattern_group = parser.add_argument_group("Pattern options")
|
|
235
|
+
pattern_group.add_argument(
|
|
165
236
|
"-e",
|
|
166
237
|
"--expression",
|
|
167
238
|
nargs="+",
|
|
@@ -171,7 +242,7 @@ def build_argument_parser() -> ArgumentParser:
|
|
|
171
242
|
" Use this option with multiple arguments if you want to seatch for"
|
|
172
243
|
" multpile patterns at once.",
|
|
173
244
|
)
|
|
174
|
-
|
|
245
|
+
pattern_group.add_argument(
|
|
175
246
|
"-f",
|
|
176
247
|
"--file",
|
|
177
248
|
nargs="+",
|
|
@@ -180,7 +251,7 @@ def build_argument_parser() -> ArgumentParser:
|
|
|
180
251
|
metavar="PATTERNFILE",
|
|
181
252
|
help="Files containing patterns (by default, regular expressions",
|
|
182
253
|
)
|
|
183
|
-
|
|
254
|
+
pattern_group.add_argument(
|
|
184
255
|
"-F",
|
|
185
256
|
"--fixed-strings",
|
|
186
257
|
action="store_true",
|
|
@@ -188,73 +259,51 @@ def build_argument_parser() -> ArgumentParser:
|
|
|
188
259
|
" expressions so special characters – .+*()[]{} – do not need to be escaped,"
|
|
189
260
|
" also saving some energy.",
|
|
190
261
|
)
|
|
191
|
-
|
|
262
|
+
pattern_group.add_argument(
|
|
192
263
|
"-i",
|
|
193
264
|
"--ignore-case",
|
|
194
265
|
action="store_false",
|
|
195
266
|
dest="case_sensitive",
|
|
196
267
|
help="case-insensitive matching",
|
|
197
268
|
)
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def build_argument_parser() -> ArgumentParser:
|
|
272
|
+
"""build the commandline argument parser"""
|
|
273
|
+
try:
|
|
274
|
+
version = metadata_version(PACKAGE_NAME)
|
|
275
|
+
except PackageNotFoundError:
|
|
276
|
+
version = get_metadata_version()
|
|
277
|
+
#
|
|
278
|
+
main_parser = ArgumentParser(
|
|
279
|
+
prog="offgrep",
|
|
280
|
+
description="searches text in (text) files recursively",
|
|
281
|
+
epilog=_EPILOG,
|
|
282
|
+
formatter_class=RawDescriptionHelpFormatter,
|
|
283
|
+
)
|
|
284
|
+
main_parser.set_defaults(
|
|
285
|
+
encoding=UTF_8,
|
|
286
|
+
hi="z",
|
|
287
|
+
pre_ignore=DEFAULT_PRE_IGNORE,
|
|
288
|
+
post_ignore=EMPTY,
|
|
204
289
|
)
|
|
290
|
+
main_parser.add_argument("-V", "--version", action="version", version=version)
|
|
205
291
|
main_parser.add_argument(
|
|
206
292
|
"--encoding",
|
|
207
293
|
help="Encoding of all files (default: %(default)s)",
|
|
208
294
|
)
|
|
295
|
+
_add_output_options(main_parser)
|
|
296
|
+
_add_file_options(main_parser)
|
|
297
|
+
_add_pattern_options(main_parser)
|
|
209
298
|
main_parser.add_argument(
|
|
210
|
-
"
|
|
211
|
-
"
|
|
212
|
-
nargs="?",
|
|
213
|
-
type=str.lower,
|
|
214
|
-
choices=[hi.name.lower() for hi in Highlighter],
|
|
215
|
-
default=Highlighter.NONE.name.lower(),
|
|
216
|
-
const=Highlighter.RED.name.lower(),
|
|
217
|
-
help="Highlight matches with the provided profile."
|
|
218
|
-
f" Defaults to {Highlighter.RED.value.enclose('%(const)s')}"
|
|
219
|
-
" if provided without a value.",
|
|
220
|
-
)
|
|
221
|
-
main_parser.add_argument(
|
|
222
|
-
"--files",
|
|
223
|
-
action="store_true",
|
|
224
|
-
help="Print the path of each file that would be searched,"
|
|
225
|
-
" without actually performing the search.",
|
|
226
|
-
)
|
|
227
|
-
main_parser.add_argument(
|
|
228
|
-
"--stats",
|
|
229
|
-
action="store_true",
|
|
230
|
-
help="Show statistics.",
|
|
231
|
-
)
|
|
232
|
-
main_parser.add_argument(
|
|
233
|
-
"--type-list",
|
|
234
|
-
action="store_true",
|
|
235
|
-
help="Print the list of supported file types. NOT IMPLEMENTED YET.",
|
|
236
|
-
)
|
|
237
|
-
main_parser.add_argument(
|
|
238
|
-
"positional_args",
|
|
239
|
-
nargs=REMAINDER,
|
|
299
|
+
"positional",
|
|
300
|
+
nargs="*",
|
|
240
301
|
help=SUPPRESS,
|
|
241
302
|
)
|
|
242
303
|
return main_parser
|
|
243
304
|
|
|
244
305
|
|
|
245
|
-
def
|
|
246
|
-
"""Remove an optins end marke if it is at the start of the paths list
|
|
247
|
-
after multiple-value options
|
|
248
|
-
"""
|
|
249
|
-
if not path_candidates:
|
|
250
|
-
return []
|
|
251
|
-
if any((args.patterns, args.patternfiles)) and path_candidates[0] == "--":
|
|
252
|
-
return path_candidates[1:]
|
|
253
|
-
#
|
|
254
|
-
return path_candidates[:]
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
def make_file_selecton_options(args: Namespace) -> FileSelectionOptions:
|
|
306
|
+
def make_file_selection_options(args: Namespace) -> FileSelectionOptions:
|
|
258
307
|
"""Initailize an FSO instance from the arguments"""
|
|
259
308
|
pre_ignore = split_without_empty_strings(args.pre_ignore)
|
|
260
309
|
post_ignore = split_without_empty_strings(args.post_ignore)
|
|
@@ -269,9 +318,7 @@ def make_file_selecton_options(args: Namespace) -> FileSelectionOptions:
|
|
|
269
318
|
#
|
|
270
319
|
#
|
|
271
320
|
if args.files:
|
|
272
|
-
paths: list[Path] = list(
|
|
273
|
-
map(Path, remove_options_end_marker(args, args.positional_args))
|
|
274
|
-
)
|
|
321
|
+
paths: list[Path] = list(map(Path, args.positional))
|
|
275
322
|
if not paths:
|
|
276
323
|
paths.append(Path.cwd())
|
|
277
324
|
#
|
|
@@ -294,6 +341,19 @@ def make_file_selecton_options(args: Namespace) -> FileSelectionOptions:
|
|
|
294
341
|
)
|
|
295
342
|
|
|
296
343
|
|
|
344
|
+
def initialize_logging(args: Namespace) -> None:
|
|
345
|
+
"""Initialize logging"""
|
|
346
|
+
loglevel = WARNING
|
|
347
|
+
if args.debug:
|
|
348
|
+
loglevel = DEBUG
|
|
349
|
+
elif args.verbose:
|
|
350
|
+
loglevel = INFO
|
|
351
|
+
elif args.quiet:
|
|
352
|
+
loglevel = CRITICAL
|
|
353
|
+
#
|
|
354
|
+
basicConfig(level=loglevel)
|
|
355
|
+
|
|
356
|
+
|
|
297
357
|
def parse_arguments() -> tuple[
|
|
298
358
|
FileSelectionOptions, PatternOptions | None, OutputOptions
|
|
299
359
|
]:
|
|
@@ -304,17 +364,26 @@ def parse_arguments() -> tuple[
|
|
|
304
364
|
"""
|
|
305
365
|
main_parser = build_argument_parser()
|
|
306
366
|
args = main_parser.parse_args()
|
|
307
|
-
|
|
367
|
+
initialize_logging(args)
|
|
308
368
|
debug(repr(args))
|
|
369
|
+
if args.highlighters:
|
|
370
|
+
print_highlighter_examples()
|
|
371
|
+
main_parser.exit()
|
|
372
|
+
#
|
|
309
373
|
if args.type_list:
|
|
310
374
|
print_supported_file_tyoes()
|
|
311
375
|
main_parser.exit()
|
|
312
376
|
#
|
|
377
|
+
produce_output = not args.quiet
|
|
313
378
|
if args.files:
|
|
314
|
-
return
|
|
379
|
+
return (
|
|
380
|
+
make_file_selection_options(args),
|
|
381
|
+
None,
|
|
382
|
+
OutputOptions(show_files=produce_output),
|
|
383
|
+
)
|
|
315
384
|
#
|
|
316
|
-
fso =
|
|
317
|
-
path_candidates = args.
|
|
385
|
+
fso = make_file_selection_options(args)
|
|
386
|
+
path_candidates = args.positional[:]
|
|
318
387
|
patterns: list[str] = []
|
|
319
388
|
if args.patterns:
|
|
320
389
|
patterns.extend(args.patterns)
|
|
@@ -328,15 +397,13 @@ def parse_arguments() -> tuple[
|
|
|
328
397
|
except IndexError:
|
|
329
398
|
main_parser.exit(
|
|
330
399
|
status=1,
|
|
331
|
-
message="
|
|
400
|
+
message="This command variant requires at least one pattern"
|
|
332
401
|
" as a positional argument.",
|
|
333
402
|
)
|
|
334
403
|
#
|
|
335
404
|
#
|
|
336
405
|
if stdin.isatty():
|
|
337
|
-
paths: list[Path] = list(
|
|
338
|
-
map(Path, remove_options_end_marker(args, path_candidates))
|
|
339
|
-
)
|
|
406
|
+
paths: list[Path] = list(map(Path, path_candidates))
|
|
340
407
|
if not paths:
|
|
341
408
|
paths.append(Path.cwd())
|
|
342
409
|
#
|
|
@@ -348,12 +415,12 @@ def parse_arguments() -> tuple[
|
|
|
348
415
|
case_sensitive=args.case_sensitive,
|
|
349
416
|
)
|
|
350
417
|
suppress_file_prefix = not fso.paths or (
|
|
351
|
-
len(fso.paths) == 1 and fso.paths[0]
|
|
418
|
+
len(fso.paths) == 1 and is_an_eligible_path(fso.paths[0])
|
|
352
419
|
)
|
|
353
420
|
oo = OutputOptions(
|
|
354
|
-
highlighter=
|
|
421
|
+
highlighter=HIGHLIGHTERS[args.hi],
|
|
355
422
|
show_files=not suppress_file_prefix,
|
|
356
|
-
show_results=
|
|
423
|
+
show_results=produce_output,
|
|
357
424
|
show_stats=args.stats,
|
|
358
425
|
)
|
|
359
426
|
return fso, po, oo
|
|
@@ -187,8 +187,11 @@ class DirectoryWalker:
|
|
|
187
187
|
effective_ignore_rulesets.extend(self.post_rulesets)
|
|
188
188
|
for contained_path in directory_contents:
|
|
189
189
|
# name = contained_path.name
|
|
190
|
-
if not (contained_path.is_dir() or contained_path
|
|
191
|
-
debug(
|
|
190
|
+
if not (contained_path.is_dir() or is_an_eligible_path(contained_path)):
|
|
191
|
+
debug(
|
|
192
|
+
"Skipping %s, not a directory, regular file, or named pipe",
|
|
193
|
+
contained_path,
|
|
194
|
+
)
|
|
192
195
|
continue
|
|
193
196
|
#
|
|
194
197
|
is_excluded = False
|
|
@@ -201,7 +204,7 @@ class DirectoryWalker:
|
|
|
201
204
|
debug("Ignoring %s", contained_path)
|
|
202
205
|
continue
|
|
203
206
|
#
|
|
204
|
-
if contained_path
|
|
207
|
+
if is_an_eligible_path(contained_path):
|
|
205
208
|
if self.use_relative_paths:
|
|
206
209
|
yield contained_path.relative_to(self.cwd)
|
|
207
210
|
else:
|
|
@@ -402,6 +405,11 @@ def determine_vcs_rules(
|
|
|
402
405
|
)
|
|
403
406
|
|
|
404
407
|
|
|
408
|
+
def is_an_eligible_path(path: Path) -> bool:
|
|
409
|
+
"""Allow regular files or named pipes"""
|
|
410
|
+
return path.is_file() or path.is_fifo()
|
|
411
|
+
|
|
412
|
+
|
|
405
413
|
def is_repository_root_path(directory: Path, repo_type: VCSDirectory) -> bool:
|
|
406
414
|
"""Check if path is the repository root path of the proviede type"""
|
|
407
415
|
path_to_check = directory / str(repo_type)
|
|
@@ -410,17 +418,17 @@ def is_repository_root_path(directory: Path, repo_type: VCSDirectory) -> bool:
|
|
|
410
418
|
|
|
411
419
|
def iter_selected_files(file_selection_opts: FileSelectionOptions) -> Iterator[Path]:
|
|
412
420
|
"""Select files"""
|
|
413
|
-
for
|
|
414
|
-
if
|
|
415
|
-
debug("Omitting ignore rules for file %s",
|
|
416
|
-
yield
|
|
417
|
-
elif
|
|
421
|
+
for current_path in file_selection_opts.paths:
|
|
422
|
+
if is_an_eligible_path(current_path):
|
|
423
|
+
debug("Omitting ignore rules for file %s", current_path)
|
|
424
|
+
yield current_path
|
|
425
|
+
elif current_path.is_dir():
|
|
418
426
|
walker = DirectoryWalker(
|
|
419
|
-
|
|
427
|
+
current_path,
|
|
420
428
|
file_selection_opts,
|
|
421
429
|
)
|
|
422
430
|
yield from walker.iter_files()
|
|
423
431
|
else:
|
|
424
|
-
warning("Ignoring %s, not a regular file",
|
|
432
|
+
warning("Ignoring %s, not a regular file", current_path)
|
|
425
433
|
#
|
|
426
434
|
#
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Output
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from functools import cached_property
|
|
9
|
+
from secrets import choice
|
|
10
|
+
from sys import stdout
|
|
11
|
+
from textwrap import indent
|
|
12
|
+
|
|
13
|
+
from .commons import EMPTY
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
STDOUT_IS_A_TTY = stdout.isatty()
|
|
17
|
+
|
|
18
|
+
_CSI = "\x1b["
|
|
19
|
+
|
|
20
|
+
## Keys
|
|
21
|
+
|
|
22
|
+
RED = "red"
|
|
23
|
+
GREEN = "green"
|
|
24
|
+
YELLOW = "yellow"
|
|
25
|
+
BLUE = "blue"
|
|
26
|
+
PURPLE = "purple"
|
|
27
|
+
CYAN = "cyan"
|
|
28
|
+
|
|
29
|
+
SUPPORTED_COLORS = (RED, GREEN, YELLOW, BLUE, PURPLE, CYAN)
|
|
30
|
+
|
|
31
|
+
LOREM_IPSUM = """Lorem ipsum dolor sit amet, consetetur sadipscing elitr,
|
|
32
|
+
sed diam nonumy eirmod tempor invidunt
|
|
33
|
+
ut labore et dolore magna aliquyam erat,
|
|
34
|
+
sed diam voluptua.
|
|
35
|
+
At vero eos et accusam et justo duo dolores et ea rebum.
|
|
36
|
+
Stet clita kasd gubergren, no sea takimata sanctus
|
|
37
|
+
est Lorem ipsum dolor sit amet."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def sgr(content: str | int) -> str:
|
|
41
|
+
"""The ANSI SGR sequence"""
|
|
42
|
+
return f"{_CSI}{content}m"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class Highlighter:
|
|
47
|
+
"""Highlighting profile"""
|
|
48
|
+
|
|
49
|
+
short: str
|
|
50
|
+
description: str
|
|
51
|
+
start: str = EMPTY
|
|
52
|
+
end: str = sgr(0)
|
|
53
|
+
apply_in_pipeline: bool = True
|
|
54
|
+
apply_on_tty: bool = True
|
|
55
|
+
|
|
56
|
+
@cached_property
|
|
57
|
+
def is_applicable(self) -> bool:
|
|
58
|
+
"""Indicate if the highlighter may be used"""
|
|
59
|
+
if STDOUT_IS_A_TTY:
|
|
60
|
+
return self.apply_on_tty
|
|
61
|
+
#
|
|
62
|
+
return self.apply_in_pipeline
|
|
63
|
+
|
|
64
|
+
@cached_property
|
|
65
|
+
def does_anything(self) -> bool:
|
|
66
|
+
"""Indicate if the highlighter defines a start and/or an end"""
|
|
67
|
+
return bool(f"{self.start}{self.end}")
|
|
68
|
+
|
|
69
|
+
def __bool__(self) -> bool:
|
|
70
|
+
"""Booelan evaluation: applicable and not empty"""
|
|
71
|
+
return self.is_applicable and self.does_anything
|
|
72
|
+
|
|
73
|
+
def enclose(self, text: str) -> str:
|
|
74
|
+
"""Return the enclosed text if possible"""
|
|
75
|
+
if self:
|
|
76
|
+
return f"{self.start}{text}{self.end}"
|
|
77
|
+
#
|
|
78
|
+
return text
|
|
79
|
+
|
|
80
|
+
def emphasize_matches(
|
|
81
|
+
self,
|
|
82
|
+
line: str,
|
|
83
|
+
*match_areas: tuple[int, int],
|
|
84
|
+
) -> str:
|
|
85
|
+
"""Return a line mith the match areas emphasized"""
|
|
86
|
+
if not self or not match_areas:
|
|
87
|
+
return line
|
|
88
|
+
#
|
|
89
|
+
pos = 0
|
|
90
|
+
collector: list[str] = []
|
|
91
|
+
marked = False
|
|
92
|
+
for a_start, a_end in sorted(match_areas):
|
|
93
|
+
if a_start > pos:
|
|
94
|
+
if pos > 0 and marked:
|
|
95
|
+
collector.append(self.end)
|
|
96
|
+
marked = False
|
|
97
|
+
#
|
|
98
|
+
collector.append(line[pos:a_start])
|
|
99
|
+
#
|
|
100
|
+
if a_start < pos:
|
|
101
|
+
a_start = pos
|
|
102
|
+
if a_end <= pos:
|
|
103
|
+
continue
|
|
104
|
+
#
|
|
105
|
+
#
|
|
106
|
+
if not marked:
|
|
107
|
+
collector.append(self.start)
|
|
108
|
+
marked = True
|
|
109
|
+
#
|
|
110
|
+
collector.append(line[a_start:a_end])
|
|
111
|
+
pos = a_end
|
|
112
|
+
#
|
|
113
|
+
if marked:
|
|
114
|
+
collector.append(self.end)
|
|
115
|
+
#
|
|
116
|
+
if pos < len(line):
|
|
117
|
+
collector.append(line[pos:])
|
|
118
|
+
#
|
|
119
|
+
return EMPTY.join(collector)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
NO_HIGHLIGHTING = Highlighter(
|
|
123
|
+
"z", "default: Zero highlighting", apply_on_tty=False, apply_in_pipeline=False
|
|
124
|
+
)
|
|
125
|
+
HIGHLIGHT_STRESSED = Highlighter(
|
|
126
|
+
"s", "Stressed text", start=sgr(1), apply_in_pipeline=False
|
|
127
|
+
)
|
|
128
|
+
HIGHLIGHT_UNDERLINE = Highlighter(
|
|
129
|
+
"u", "Underlined text", start=sgr(4), apply_in_pipeline=False
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
HIGHLIGHTERS: dict[str, Highlighter] = {
|
|
133
|
+
instance.short: instance
|
|
134
|
+
for instance in [
|
|
135
|
+
NO_HIGHLIGHTING,
|
|
136
|
+
Highlighter("a", "text enclosed in Arrows", start="→", end="←"),
|
|
137
|
+
Highlighter(".", "text enclosed in POINTing hand emojis", start="👉", end="👈"),
|
|
138
|
+
HIGHLIGHT_STRESSED,
|
|
139
|
+
HIGHLIGHT_UNDERLINE,
|
|
140
|
+
]
|
|
141
|
+
+ [
|
|
142
|
+
Highlighter(
|
|
143
|
+
color_name[0],
|
|
144
|
+
f"{color_name.title()} colored text",
|
|
145
|
+
sgr(sgr_index),
|
|
146
|
+
apply_in_pipeline=False,
|
|
147
|
+
)
|
|
148
|
+
for sgr_index, color_name in enumerate(SUPPORTED_COLORS, start=31)
|
|
149
|
+
]
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclass
|
|
154
|
+
class OutputOptions:
|
|
155
|
+
"""Options for pattern matching"""
|
|
156
|
+
|
|
157
|
+
highlighter: Highlighter = NO_HIGHLIGHTING
|
|
158
|
+
show_files: bool = True
|
|
159
|
+
show_results: bool = True
|
|
160
|
+
show_stats: bool = False
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def single_example(highlighter: Highlighter, text: str) -> str:
|
|
164
|
+
"""split text into words, and apply highlighting to a randow word"""
|
|
165
|
+
words = text.split()
|
|
166
|
+
random_word = choice(words).strip(",.")
|
|
167
|
+
splitted_text = text.split(random_word)
|
|
168
|
+
return highlighter.enclose(random_word).join(splitted_text)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def print_highlighter_examples() -> None:
|
|
172
|
+
"""Print examples of all highlighters"""
|
|
173
|
+
print(HIGHLIGHT_STRESSED.enclose("– Highlighters showcase –\n"))
|
|
174
|
+
lorem_ipsum_lines = LOREM_IPSUM.splitlines()
|
|
175
|
+
for key, highlighter in sorted(HIGHLIGHTERS.items()):
|
|
176
|
+
random_line = choice(lorem_ipsum_lines)
|
|
177
|
+
stressed = HIGHLIGHT_STRESSED.enclose(repr(key))
|
|
178
|
+
addition = (
|
|
179
|
+
EMPTY
|
|
180
|
+
if highlighter.apply_in_pipeline or not highlighter
|
|
181
|
+
else ", not applied if stdout is redirected"
|
|
182
|
+
)
|
|
183
|
+
print(f"{stressed} ({highlighter.description}{addition}):")
|
|
184
|
+
print(indent(f"\n{single_example(highlighter, random_line)}\n", " > "))
|
|
185
|
+
#
|
|
@@ -6,26 +6,22 @@ Pattern matching
|
|
|
6
6
|
|
|
7
7
|
from collections import Counter
|
|
8
8
|
from dataclasses import dataclass, field
|
|
9
|
-
from enum import Enum
|
|
10
9
|
from logging import debug, error
|
|
11
10
|
from pathlib import Path
|
|
12
11
|
from re import compile as re_compile, Pattern, IGNORECASE
|
|
13
|
-
from sys import stdin
|
|
12
|
+
from sys import stdin
|
|
14
13
|
from time import monotonic
|
|
15
14
|
from typing import TextIO, Any
|
|
16
15
|
|
|
17
|
-
from .commons import
|
|
16
|
+
from .commons import UTF_8
|
|
17
|
+
from .output import (
|
|
18
|
+
OutputOptions,
|
|
19
|
+
Highlighter,
|
|
20
|
+
HIGHLIGHT_STRESSED,
|
|
21
|
+
NO_HIGHLIGHTING,
|
|
22
|
+
)
|
|
18
23
|
|
|
19
24
|
|
|
20
|
-
STDOUT_IS_A_TTY = stdout.isatty()
|
|
21
|
-
|
|
22
|
-
PACKAGE_NAME = "offgrep"
|
|
23
|
-
"""The package name for metadata inspection"""
|
|
24
|
-
ANSI_HI = "\x1b[1m"
|
|
25
|
-
ANSI_NORMAL = "\x1b[0m"
|
|
26
|
-
ANSI_COLOR_RED = "\x1b[31m"
|
|
27
|
-
ANSI_COLOR_GREEN = "\x1b[32m"
|
|
28
|
-
|
|
29
25
|
_UNDERLINE = "_"
|
|
30
26
|
_SPACE = " "
|
|
31
27
|
|
|
@@ -33,53 +29,6 @@ _FILES_WITH_MATCHES = "files_with_matches"
|
|
|
33
29
|
_TOTAL_MATCHES = "total_matches"
|
|
34
30
|
|
|
35
31
|
|
|
36
|
-
@dataclass(frozen=True)
|
|
37
|
-
class HighlightProfile:
|
|
38
|
-
"""Highlighting profile"""
|
|
39
|
-
|
|
40
|
-
start: str = EMPTY
|
|
41
|
-
end: str = EMPTY
|
|
42
|
-
requires_tty: bool = True
|
|
43
|
-
|
|
44
|
-
@property
|
|
45
|
-
def is_applicable(self) -> bool:
|
|
46
|
-
"""Indicate if the highlighter can be used"""
|
|
47
|
-
return not self.requires_tty or STDOUT_IS_A_TTY
|
|
48
|
-
|
|
49
|
-
def __bool__(self) -> bool:
|
|
50
|
-
"""Booelan evaluation: not empty"""
|
|
51
|
-
return bool(f"{self.start}{self.end}")
|
|
52
|
-
|
|
53
|
-
def enclose(self, text: str) -> str:
|
|
54
|
-
"""Return the enclosed text if possible"""
|
|
55
|
-
if self.is_applicable:
|
|
56
|
-
return f"{self.start}{text}{self.end}"
|
|
57
|
-
#
|
|
58
|
-
return text
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
class Highlighter(Enum):
|
|
62
|
-
"""Highlighting profiles container"""
|
|
63
|
-
|
|
64
|
-
ARROWS = HighlightProfile(start="➡️", end="⬅️", requires_tty=False)
|
|
65
|
-
GREEN = HighlightProfile(start=ANSI_COLOR_GREEN, end=ANSI_NORMAL)
|
|
66
|
-
NONE = HighlightProfile()
|
|
67
|
-
POINTING = HighlightProfile(start="👉", end="👈", requires_tty=False)
|
|
68
|
-
PUSHING = HighlightProfile(start="🫸", end="🫷", requires_tty=False)
|
|
69
|
-
RED = HighlightProfile(start=ANSI_COLOR_RED, end=ANSI_NORMAL)
|
|
70
|
-
STRESSED = HighlightProfile(start=ANSI_HI, end=ANSI_NORMAL)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
@dataclass
|
|
74
|
-
class OutputOptions:
|
|
75
|
-
"""Options for pattern matching"""
|
|
76
|
-
|
|
77
|
-
highlighter: Highlighter = Highlighter.NONE
|
|
78
|
-
show_files: bool = True
|
|
79
|
-
show_results: bool = True
|
|
80
|
-
show_stats: bool = True
|
|
81
|
-
|
|
82
|
-
|
|
83
32
|
@dataclass
|
|
84
33
|
class PatternOptions:
|
|
85
34
|
"""Options for pattern matching"""
|
|
@@ -230,53 +179,11 @@ class MultiPatternsMatcher:
|
|
|
230
179
|
return []
|
|
231
180
|
|
|
232
181
|
|
|
233
|
-
def highlight_line_matches(
|
|
234
|
-
line: str,
|
|
235
|
-
line_areas: list[tuple[int, int]],
|
|
236
|
-
highlighter: Highlighter = Highlighter.RED,
|
|
237
|
-
) -> str:
|
|
238
|
-
"""Return a colorized line"""
|
|
239
|
-
if not highlighter.value.is_applicable:
|
|
240
|
-
return line
|
|
241
|
-
#
|
|
242
|
-
pos = 0
|
|
243
|
-
collector: list[str] = []
|
|
244
|
-
marked = False
|
|
245
|
-
for a_start, a_end in line_areas:
|
|
246
|
-
if a_start > pos:
|
|
247
|
-
if pos > 0 and marked:
|
|
248
|
-
collector.append(highlighter.value.end)
|
|
249
|
-
marked = False
|
|
250
|
-
#
|
|
251
|
-
collector.append(line[pos:a_start])
|
|
252
|
-
#
|
|
253
|
-
if a_start < pos:
|
|
254
|
-
a_start = pos
|
|
255
|
-
if a_end <= pos:
|
|
256
|
-
continue
|
|
257
|
-
#
|
|
258
|
-
#
|
|
259
|
-
if not marked:
|
|
260
|
-
collector.append(highlighter.value.start)
|
|
261
|
-
marked = True
|
|
262
|
-
#
|
|
263
|
-
collector.append(line[a_start:a_end])
|
|
264
|
-
pos = a_end
|
|
265
|
-
#
|
|
266
|
-
if marked:
|
|
267
|
-
collector.append(highlighter.value.end)
|
|
268
|
-
#
|
|
269
|
-
if pos < len(line):
|
|
270
|
-
collector.append(line[pos:])
|
|
271
|
-
#
|
|
272
|
-
return EMPTY.join(collector)
|
|
273
|
-
|
|
274
|
-
|
|
275
182
|
def find_in_stream(
|
|
276
183
|
input_stream: TextIO | Any,
|
|
277
184
|
mpm: MultiPatternsMatcher,
|
|
278
185
|
collect_stats: bool = False,
|
|
279
|
-
highlighter: Highlighter =
|
|
186
|
+
highlighter: Highlighter = NO_HIGHLIGHTING,
|
|
280
187
|
) -> tuple[int, list[tuple[int, str]]]:
|
|
281
188
|
"""find the patterns in a single file"""
|
|
282
189
|
count = 0
|
|
@@ -290,8 +197,9 @@ def find_in_stream(
|
|
|
290
197
|
findings.append(
|
|
291
198
|
(
|
|
292
199
|
lineno,
|
|
293
|
-
|
|
294
|
-
line.rstrip(),
|
|
200
|
+
highlighter.emphasize_matches(
|
|
201
|
+
line.rstrip(),
|
|
202
|
+
*sorted(set(line_result)),
|
|
295
203
|
),
|
|
296
204
|
)
|
|
297
205
|
)
|
|
@@ -326,6 +234,7 @@ def process_results(
|
|
|
326
234
|
"""process single stream matching results:
|
|
327
235
|
modify stats in place, and print findings
|
|
328
236
|
"""
|
|
237
|
+
stats.update(files_processed=1)
|
|
329
238
|
if count:
|
|
330
239
|
previously_matched_files = stats[_FILES_WITH_MATCHES]
|
|
331
240
|
stats.update(
|
|
@@ -340,7 +249,7 @@ def process_results(
|
|
|
340
249
|
if previously_matched_files:
|
|
341
250
|
print()
|
|
342
251
|
#
|
|
343
|
-
print(
|
|
252
|
+
print(HIGHLIGHT_STRESSED.enclose(f"{file_name} ::"))
|
|
344
253
|
#
|
|
345
254
|
for lineno, line in findings:
|
|
346
255
|
print(f"{lineno:{number_width}d}: {line}")
|
|
@@ -353,7 +262,7 @@ def show_stats(stats: Counter[str], duration: float) -> None:
|
|
|
353
262
|
if stats[_TOTAL_MATCHES]:
|
|
354
263
|
print()
|
|
355
264
|
#
|
|
356
|
-
print(
|
|
265
|
+
print(HIGHLIGHT_STRESSED.enclose("⁘ Statistics ⁘"))
|
|
357
266
|
for key, value in sorted(stats.items()):
|
|
358
267
|
print(f"{value:>6d} {key.replace(_UNDERLINE, _SPACE)}")
|
|
359
268
|
#
|
|
@@ -407,7 +316,6 @@ def find_in_files(
|
|
|
407
316
|
stats.update(errors=1)
|
|
408
317
|
continue
|
|
409
318
|
#
|
|
410
|
-
stats.update(files_processed=1)
|
|
411
319
|
process_results(output_opts, stats, str(single_file), count, findings)
|
|
412
320
|
#
|
|
413
321
|
#
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|