offgrep 0.1.1__tar.gz → 0.3.0__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.1.1 → offgrep-0.3.0}/PKG-INFO +2 -3
- {offgrep-0.1.1 → offgrep-0.3.0}/README.md +1 -2
- {offgrep-0.1.1 → offgrep-0.3.0}/pyproject.toml +1 -1
- offgrep-0.3.0/src/offgrep/__main__.py +50 -0
- offgrep-0.3.0/src/offgrep/arguments.py +359 -0
- {offgrep-0.1.1 → offgrep-0.3.0}/src/offgrep/commons.py +5 -0
- offgrep-0.3.0/src/offgrep/filesystem.py +426 -0
- {offgrep-0.1.1 → offgrep-0.3.0}/src/offgrep/git.py +0 -3
- offgrep-0.1.1/src/offgrep/ignore.py → offgrep-0.3.0/src/offgrep/path_matching.py +215 -89
- offgrep-0.3.0/src/offgrep/pattern_matching.py +418 -0
- offgrep-0.1.1/src/offgrep/__main__.py +0 -230
- offgrep-0.1.1/src/offgrep/filesystem.py +0 -182
- {offgrep-0.1.1 → offgrep-0.3.0}/LICENSE +0 -0
- {offgrep-0.1.1 → offgrep-0.3.0}/src/offgrep/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: offgrep
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A lousy ripgrep rip-off in pure Python
|
|
5
5
|
Keywords: grep,recursive
|
|
6
6
|
Author: Rainer Schwarzbach
|
|
@@ -67,5 +67,4 @@ instead of **offgrep**.
|
|
|
67
67
|
|
|
68
68
|
If you – however – are in a super restricted network (eg. government)
|
|
69
69
|
without a chance to install **ripgrep**,
|
|
70
|
-
but are able to install Python packages, **offgrep** might be
|
|
71
|
-
alterative.
|
|
70
|
+
but are able to install Python packages, **offgrep** might be suitable for you.
|
|
@@ -18,5 +18,4 @@ instead of **offgrep**.
|
|
|
18
18
|
|
|
19
19
|
If you – however – are in a super restricted network (eg. government)
|
|
20
20
|
without a chance to install **ripgrep**,
|
|
21
|
-
but are able to install Python packages, **offgrep** might be
|
|
22
|
-
alterative.
|
|
21
|
+
but are able to install Python packages, **offgrep** might be suitable for you.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Call as script
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from .arguments import parse_arguments
|
|
10
|
+
from .filesystem import FileSelectionOptions, iter_selected_files
|
|
11
|
+
from .pattern_matching import PatternOptions, OutputOptions, find_in_files
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def grep_files(
|
|
15
|
+
file_selection_opts: FileSelectionOptions,
|
|
16
|
+
pattern_opts: PatternOptions,
|
|
17
|
+
output_opts: OutputOptions,
|
|
18
|
+
) -> int:
|
|
19
|
+
"""Search for text in files"""
|
|
20
|
+
target_paths = list(iter_selected_files(file_selection_opts))
|
|
21
|
+
return find_in_files(
|
|
22
|
+
target_paths, pattern_opts, output_opts, encoding=file_selection_opts.encoding
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def list_files(file_selection_opts: FileSelectionOptions) -> int:
|
|
27
|
+
"""List files that would be searched"""
|
|
28
|
+
for single_file in iter_selected_files(file_selection_opts):
|
|
29
|
+
print(single_file)
|
|
30
|
+
#
|
|
31
|
+
return 0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def main() -> int:
|
|
35
|
+
"""Main script interface
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
the script returncode
|
|
39
|
+
"""
|
|
40
|
+
file_selection_opts, maybe_pattern_opts, output_opts = parse_arguments()
|
|
41
|
+
if isinstance(maybe_pattern_opts, PatternOptions):
|
|
42
|
+
return grep_files(file_selection_opts, maybe_pattern_opts, output_opts)
|
|
43
|
+
#
|
|
44
|
+
return list_files(file_selection_opts)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
if __name__ == "__main__":
|
|
48
|
+
sys.exit(main()) # pragma: no cover
|
|
49
|
+
|
|
50
|
+
# vim: fileencoding=utf-8 ts=4 sts=4 sw=4 autoindent expandtab syntax=python:
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Command line argument parsing
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from argparse import (
|
|
8
|
+
ArgumentParser,
|
|
9
|
+
Namespace,
|
|
10
|
+
RawDescriptionHelpFormatter,
|
|
11
|
+
REMAINDER,
|
|
12
|
+
SUPPRESS,
|
|
13
|
+
)
|
|
14
|
+
from importlib.metadata import PackageNotFoundError, version as metadata_version
|
|
15
|
+
from logging import DEBUG, WARNING, basicConfig, debug, warning
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from sys import stdin
|
|
18
|
+
from tomllib import loads as tomli_loads
|
|
19
|
+
|
|
20
|
+
from .commons import EMPTY, UTF_8, split_without_empty_strings
|
|
21
|
+
from .filesystem import FileSelectionOptions, IgnoreFileMode, Priority, VCSDirectory
|
|
22
|
+
from .pattern_matching import Highlighter, PatternOptions, OutputOptions
|
|
23
|
+
|
|
24
|
+
DEFAULT_PRE_IGNORE = ",".join(f"{dirname}/" for dirname in VCSDirectory)
|
|
25
|
+
|
|
26
|
+
PACKAGE_NAME = "offgrep"
|
|
27
|
+
"""The package name for metadata inspection"""
|
|
28
|
+
|
|
29
|
+
_EPILOG = """The provided options (or usage in a shell pipeline)
|
|
30
|
+
determine how the positional arguments are interpreted:
|
|
31
|
+
|
|
32
|
+
(1) offgrep [OPTIONS] PATTERN [PATH ...],
|
|
33
|
+
the first positional argument is interpreted as the pattern,
|
|
34
|
+
the remaining ones as paths.
|
|
35
|
+
(2) offgrep [OPTIONS] -e PATTERN ... [PATH ...]
|
|
36
|
+
all positional argument are interpreted as paths.
|
|
37
|
+
(3) offgrep [OPTIONS] -f PATTERNFILE ... [PATH ...]
|
|
38
|
+
all positional argument are interpreted as paths.
|
|
39
|
+
(4) offgrep [OPTIONS] --files [PATH ...]
|
|
40
|
+
all positional argument are interpreted as paths.
|
|
41
|
+
(5) offgrep [OPTIONS] --type-list
|
|
42
|
+
all positional arguments are ignored.
|
|
43
|
+
(6) command | offgrep [OPTIONS] PATTERN
|
|
44
|
+
the first positional argument is interpreted as the pattern
|
|
45
|
+
if no pattern has been provided through -e or -f,
|
|
46
|
+
the remaining positional arguments are ignored.
|
|
47
|
+
|
|
48
|
+
If no paths were provided as positional arguments in scenarios 1 through 4,
|
|
49
|
+
the current working directory will be used as the starting point.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_file_contents(file_name: str, up_dirs: int = 0) -> str:
|
|
54
|
+
"""get contents of a file in an ancestor directory
|
|
55
|
+
of the current file
|
|
56
|
+
|
|
57
|
+
...
|
|
58
|
+
"""
|
|
59
|
+
file_dir_path = Path(__file__).resolve()
|
|
60
|
+
for _ in range(up_dirs):
|
|
61
|
+
file_dir_path = file_dir_path.parent
|
|
62
|
+
#
|
|
63
|
+
file_path = file_dir_path / file_name
|
|
64
|
+
return file_path.read_text(encoding=UTF_8)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_metadata_version(
|
|
68
|
+
metadata_file_name: str = "pyproject.toml", up_dirs: int = 3
|
|
69
|
+
) -> str:
|
|
70
|
+
"""get version information from _metadata_file_name_ in an ancestor directory
|
|
71
|
+
|
|
72
|
+
...
|
|
73
|
+
"""
|
|
74
|
+
try:
|
|
75
|
+
metadata_file_contents = get_file_contents(metadata_file_name, up_dirs=up_dirs)
|
|
76
|
+
except IOError as io_error:
|
|
77
|
+
return str(io_error)
|
|
78
|
+
#
|
|
79
|
+
metadata = tomli_loads(metadata_file_contents)
|
|
80
|
+
try:
|
|
81
|
+
# logging.warning(str(metadata.items()))
|
|
82
|
+
version = metadata["project"]["version"]
|
|
83
|
+
except KeyError:
|
|
84
|
+
return f"Error: no version information in metadata from {metadata_file_name}"
|
|
85
|
+
#
|
|
86
|
+
return f"{version} (read directly from {metadata_file_name})"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def determine_ignore_file_mode() -> IgnoreFileMode:
|
|
90
|
+
"""dummy for now"""
|
|
91
|
+
return IgnoreFileMode.AUTOMATIC
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def determine_types(types_arg: list[str] | None) -> list[str]:
|
|
95
|
+
"""dummy for now"""
|
|
96
|
+
if isinstance(types_arg, list):
|
|
97
|
+
for single_type in types_arg:
|
|
98
|
+
warning("--type %s option not implemented yet", single_type)
|
|
99
|
+
#
|
|
100
|
+
#
|
|
101
|
+
return []
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def print_supported_file_tyoes() -> None:
|
|
105
|
+
"""Print the list of supported file types"""
|
|
106
|
+
warning("--type-list option not implemented yet")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def build_argument_parser() -> ArgumentParser:
|
|
110
|
+
"""build the commandline argument parser"""
|
|
111
|
+
try:
|
|
112
|
+
version = metadata_version(PACKAGE_NAME)
|
|
113
|
+
except PackageNotFoundError:
|
|
114
|
+
version = get_metadata_version()
|
|
115
|
+
#
|
|
116
|
+
main_parser = ArgumentParser(
|
|
117
|
+
prog="offgrep",
|
|
118
|
+
description="searches text in (text) files recursively",
|
|
119
|
+
epilog=_EPILOG,
|
|
120
|
+
formatter_class=RawDescriptionHelpFormatter,
|
|
121
|
+
)
|
|
122
|
+
main_parser.set_defaults(
|
|
123
|
+
loglevel=WARNING,
|
|
124
|
+
encoding=UTF_8,
|
|
125
|
+
pre_ignore=DEFAULT_PRE_IGNORE,
|
|
126
|
+
post_ignore=EMPTY,
|
|
127
|
+
)
|
|
128
|
+
main_parser.add_argument("-V", "--version", action="version", version=version)
|
|
129
|
+
main_parser.add_argument(
|
|
130
|
+
"--debug",
|
|
131
|
+
action="store_const",
|
|
132
|
+
const=DEBUG,
|
|
133
|
+
dest="loglevel",
|
|
134
|
+
help="log with debug level",
|
|
135
|
+
)
|
|
136
|
+
main_parser.add_argument(
|
|
137
|
+
"--default-ignore-dirs",
|
|
138
|
+
help="Always ignore the directories named here, separated by commas."
|
|
139
|
+
" DEPRECATED, please use --pre-ignore or --post-ignore instead.",
|
|
140
|
+
)
|
|
141
|
+
main_parser.add_argument(
|
|
142
|
+
"--default-ignore-priority",
|
|
143
|
+
choices=list(Priority),
|
|
144
|
+
type=Priority,
|
|
145
|
+
help="Priority for the ignore rules specified through -default-ignore-dirs."
|
|
146
|
+
f" Choices: %(choices)s ({str(Priority.LOW)!r} is applied first but might"
|
|
147
|
+
" be overridden by eg. directory defined rules,"
|
|
148
|
+
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.",
|
|
151
|
+
)
|
|
152
|
+
main_parser.add_argument(
|
|
153
|
+
"--pre-ignore",
|
|
154
|
+
help="gitignore-style patterns for files and/or directories ignored"
|
|
155
|
+
" before applying found ignore file rules, joined with commas (,)."
|
|
156
|
+
" Defaults to VCS repository directories (%(default)s).",
|
|
157
|
+
)
|
|
158
|
+
main_parser.add_argument(
|
|
159
|
+
"--post-ignore",
|
|
160
|
+
help="gitignore-style patterns for files and/or directories ignored"
|
|
161
|
+
" after applying found ignore file rules, joined with commas (,)."
|
|
162
|
+
" Empty by default (%(default)s).",
|
|
163
|
+
)
|
|
164
|
+
main_parser.add_argument(
|
|
165
|
+
"-e",
|
|
166
|
+
"--expression",
|
|
167
|
+
nargs="+",
|
|
168
|
+
dest="patterns",
|
|
169
|
+
metavar="PATTERN",
|
|
170
|
+
help="Patterns (by default, reular exoressions) to search for."
|
|
171
|
+
" Use this option with multiple arguments if you want to seatch for"
|
|
172
|
+
" multpile patterns at once.",
|
|
173
|
+
)
|
|
174
|
+
main_parser.add_argument(
|
|
175
|
+
"-f",
|
|
176
|
+
"--file",
|
|
177
|
+
nargs="+",
|
|
178
|
+
type=Path,
|
|
179
|
+
dest="patternfiles",
|
|
180
|
+
metavar="PATTERNFILE",
|
|
181
|
+
help="Files containing patterns (by default, regular expressions",
|
|
182
|
+
)
|
|
183
|
+
main_parser.add_argument(
|
|
184
|
+
"-F",
|
|
185
|
+
"--fixed-strings",
|
|
186
|
+
action="store_true",
|
|
187
|
+
help="treat the given pattern(s) as fixed strings instead of regular"
|
|
188
|
+
" expressions so special characters – .+*()[]{} – do not need to be escaped,"
|
|
189
|
+
" also saving some energy.",
|
|
190
|
+
)
|
|
191
|
+
main_parser.add_argument(
|
|
192
|
+
"-i",
|
|
193
|
+
"--ignore-case",
|
|
194
|
+
action="store_false",
|
|
195
|
+
dest="case_sensitive",
|
|
196
|
+
help="case-insensitive matching",
|
|
197
|
+
)
|
|
198
|
+
main_parser.add_argument(
|
|
199
|
+
"-t",
|
|
200
|
+
"--type",
|
|
201
|
+
nargs="+",
|
|
202
|
+
help="search only in files of type %(metavar)s. NOT IMPLEMENTED YET.",
|
|
203
|
+
# See --type-list output for supported file types.",
|
|
204
|
+
)
|
|
205
|
+
main_parser.add_argument(
|
|
206
|
+
"--encoding",
|
|
207
|
+
help="Encoding of all files (default: %(default)s)",
|
|
208
|
+
)
|
|
209
|
+
main_parser.add_argument(
|
|
210
|
+
"--highlight-matches",
|
|
211
|
+
"--hm",
|
|
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,
|
|
240
|
+
help=SUPPRESS,
|
|
241
|
+
)
|
|
242
|
+
return main_parser
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def remove_options_end_marker(args: Namespace, path_candidates: list[str]) -> list[str]:
|
|
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:
|
|
258
|
+
"""Initailize an FSO instance from the arguments"""
|
|
259
|
+
pre_ignore = split_without_empty_strings(args.pre_ignore)
|
|
260
|
+
post_ignore = split_without_empty_strings(args.post_ignore)
|
|
261
|
+
if args.default_ignore_dirs:
|
|
262
|
+
ignore_list = [f"{dirname}/" for dirname in args.default_ignore_dirs]
|
|
263
|
+
match args.default_ignore_priority:
|
|
264
|
+
case Priority.LOW:
|
|
265
|
+
pre_ignore = ignore_list
|
|
266
|
+
case Priority.HIGH:
|
|
267
|
+
post_ignore = ignore_list
|
|
268
|
+
#
|
|
269
|
+
#
|
|
270
|
+
#
|
|
271
|
+
if args.files:
|
|
272
|
+
paths: list[Path] = list(
|
|
273
|
+
map(Path, remove_options_end_marker(args, args.positional_args))
|
|
274
|
+
)
|
|
275
|
+
if not paths:
|
|
276
|
+
paths.append(Path.cwd())
|
|
277
|
+
#
|
|
278
|
+
return FileSelectionOptions(
|
|
279
|
+
paths=paths,
|
|
280
|
+
pre_ignore=pre_ignore,
|
|
281
|
+
post_ignore=post_ignore,
|
|
282
|
+
types=determine_types(args.type),
|
|
283
|
+
ignore_file_mode=determine_ignore_file_mode(),
|
|
284
|
+
encoding=args.encoding,
|
|
285
|
+
)
|
|
286
|
+
#
|
|
287
|
+
return FileSelectionOptions(
|
|
288
|
+
paths=[],
|
|
289
|
+
pre_ignore=pre_ignore,
|
|
290
|
+
post_ignore=post_ignore,
|
|
291
|
+
types=determine_types(args.type),
|
|
292
|
+
ignore_file_mode=determine_ignore_file_mode(),
|
|
293
|
+
encoding=args.encoding,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def parse_arguments() -> tuple[
|
|
298
|
+
FileSelectionOptions, PatternOptions | None, OutputOptions
|
|
299
|
+
]:
|
|
300
|
+
"""Parse commandline arguments and configure logging
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
the parsed command line arguments
|
|
304
|
+
"""
|
|
305
|
+
main_parser = build_argument_parser()
|
|
306
|
+
args = main_parser.parse_args()
|
|
307
|
+
basicConfig(level=args.loglevel)
|
|
308
|
+
debug(repr(args))
|
|
309
|
+
if args.type_list:
|
|
310
|
+
print_supported_file_tyoes()
|
|
311
|
+
main_parser.exit()
|
|
312
|
+
#
|
|
313
|
+
if args.files:
|
|
314
|
+
return make_file_selecton_options(args), None, OutputOptions()
|
|
315
|
+
#
|
|
316
|
+
fso = make_file_selecton_options(args)
|
|
317
|
+
path_candidates = args.positional_args[:]
|
|
318
|
+
patterns: list[str] = []
|
|
319
|
+
if args.patterns:
|
|
320
|
+
patterns.extend(args.patterns)
|
|
321
|
+
elif args.patternfiles:
|
|
322
|
+
for single_file in args.patternfiles:
|
|
323
|
+
patterns.extend(single_file.read_text(encoding=args.encoding).splitlines())
|
|
324
|
+
#
|
|
325
|
+
else:
|
|
326
|
+
try:
|
|
327
|
+
patterns = [path_candidates.pop(0)]
|
|
328
|
+
except IndexError:
|
|
329
|
+
main_parser.exit(
|
|
330
|
+
status=1,
|
|
331
|
+
message="Using offgrep requires at least one pattern"
|
|
332
|
+
" as a positional argument.",
|
|
333
|
+
)
|
|
334
|
+
#
|
|
335
|
+
#
|
|
336
|
+
if stdin.isatty():
|
|
337
|
+
paths: list[Path] = list(
|
|
338
|
+
map(Path, remove_options_end_marker(args, path_candidates))
|
|
339
|
+
)
|
|
340
|
+
if not paths:
|
|
341
|
+
paths.append(Path.cwd())
|
|
342
|
+
#
|
|
343
|
+
fso.paths = paths
|
|
344
|
+
#
|
|
345
|
+
po = PatternOptions(
|
|
346
|
+
patterns=patterns,
|
|
347
|
+
plaintext=args.fixed_strings,
|
|
348
|
+
case_sensitive=args.case_sensitive,
|
|
349
|
+
)
|
|
350
|
+
suppress_file_prefix = not fso.paths or (
|
|
351
|
+
len(fso.paths) == 1 and fso.paths[0].is_file()
|
|
352
|
+
)
|
|
353
|
+
oo = OutputOptions(
|
|
354
|
+
highlighter=Highlighter[args.highlight_matches.upper()],
|
|
355
|
+
show_files=not suppress_file_prefix,
|
|
356
|
+
show_results=True,
|
|
357
|
+
show_stats=args.stats,
|
|
358
|
+
)
|
|
359
|
+
return fso, po, oo
|
|
@@ -39,3 +39,8 @@ SLASH = "/"
|
|
|
39
39
|
|
|
40
40
|
UTF_8 = "utf-8"
|
|
41
41
|
"""Explicit default encoding"""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def split_without_empty_strings(source: str, delimiter: str = ",") -> list[str]:
|
|
45
|
+
"""Split result without empty strings"""
|
|
46
|
+
return [part for part in source.split(delimiter) if part]
|