pseek 2.5.1__tar.gz → 2.5.3__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.
- {pseek-2.5.1 → pseek-2.5.3}/PKG-INFO +3 -2
- {pseek-2.5.1 → pseek-2.5.3}/README.md +2 -1
- {pseek-2.5.1 → pseek-2.5.3}/pseek/cli.py +5 -2
- {pseek-2.5.1 → pseek-2.5.3}/pseek/searcher.py +199 -142
- {pseek-2.5.1 → pseek-2.5.3}/pseek/utils.py +4 -0
- {pseek-2.5.1 → pseek-2.5.3}/pseek.egg-info/PKG-INFO +3 -2
- {pseek-2.5.1 → pseek-2.5.3}/pyproject.toml +1 -1
- {pseek-2.5.1 → pseek-2.5.3}/LICENSE +0 -0
- {pseek-2.5.1 → pseek-2.5.3}/pseek/__init__.py +0 -0
- {pseek-2.5.1 → pseek-2.5.3}/pseek/parser.py +0 -0
- {pseek-2.5.1 → pseek-2.5.3}/pseek.egg-info/SOURCES.txt +0 -0
- {pseek-2.5.1 → pseek-2.5.3}/pseek.egg-info/dependency_links.txt +0 -0
- {pseek-2.5.1 → pseek-2.5.3}/pseek.egg-info/entry_points.txt +0 -0
- {pseek-2.5.1 → pseek-2.5.3}/pseek.egg-info/requires.txt +0 -0
- {pseek-2.5.1 → pseek-2.5.3}/pseek.egg-info/top_level.txt +0 -0
- {pseek-2.5.1 → pseek-2.5.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pseek
|
|
3
|
-
Version: 2.5.
|
|
3
|
+
Version: 2.5.3
|
|
4
4
|
Summary: Pseek is a Python library to search files, folders, and text
|
|
5
5
|
Author-email: Arian <ariannasiri86@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -102,6 +102,7 @@ pseek "error\d+" --regex
|
|
|
102
102
|
| `--fuzzy_level` | Similarity threshold from 0 to 99 for fuzzy search (default: `80`) |
|
|
103
103
|
| `--max-size`, `--min-size` | Specify maximum and minimum sizes for files and directories |
|
|
104
104
|
| `--archive` | Enable search within archive files (e.g. `zip`, `rar`, `7z`, `gz`, `bz2`, `xz`, `tar`, `tar.gz`, `tar.bz2`, `tar.xz`) |
|
|
105
|
+
| `--depth` | Maximum archive depth to recurse into (e.g. 2 means only 2 levels) |
|
|
105
106
|
| `--arc-ext`, `--arc-ee` | Filter by file extension inside archive files |
|
|
106
107
|
| `--arc-inc`, `--arc-exc` | Limit search results to specific set of directories or files inside archive files |
|
|
107
108
|
| `--arc-max`, `--arc-min` | Specify maximum and minimum sizes for files inside archive files (It doesn't work for directories because their size is zero in archive files) |
|
|
@@ -112,4 +113,4 @@ pseek "error\d+" --regex
|
|
|
112
113
|
## Requirements
|
|
113
114
|
|
|
114
115
|
* Python 3.6 or higher
|
|
115
|
-
* `unrar`, `bsdtar`, `unar` or `7zip` for the rarfile library to support searching inside `.rar` files (optional)
|
|
116
|
+
* `unrar`, `bsdtar`, `unar` or `7zip` for the [rarfile](https://pypi.org/project/rarfile/) library to support searching inside `.rar` files (optional)
|
|
@@ -85,6 +85,7 @@ pseek "error\d+" --regex
|
|
|
85
85
|
| `--fuzzy_level` | Similarity threshold from 0 to 99 for fuzzy search (default: `80`) |
|
|
86
86
|
| `--max-size`, `--min-size` | Specify maximum and minimum sizes for files and directories |
|
|
87
87
|
| `--archive` | Enable search within archive files (e.g. `zip`, `rar`, `7z`, `gz`, `bz2`, `xz`, `tar`, `tar.gz`, `tar.bz2`, `tar.xz`) |
|
|
88
|
+
| `--depth` | Maximum archive depth to recurse into (e.g. 2 means only 2 levels) |
|
|
88
89
|
| `--arc-ext`, `--arc-ee` | Filter by file extension inside archive files |
|
|
89
90
|
| `--arc-inc`, `--arc-exc` | Limit search results to specific set of directories or files inside archive files |
|
|
90
91
|
| `--arc-max`, `--arc-min` | Specify maximum and minimum sizes for files inside archive files (It doesn't work for directories because their size is zero in archive files) |
|
|
@@ -95,4 +96,4 @@ pseek "error\d+" --regex
|
|
|
95
96
|
## Requirements
|
|
96
97
|
|
|
97
98
|
* Python 3.6 or higher
|
|
98
|
-
* `unrar`, `bsdtar`, `unar` or `7zip` for the rarfile library to support searching inside `.rar` files (optional)
|
|
99
|
+
* `unrar`, `bsdtar`, `unar` or `7zip` for the [rarfile](https://pypi.org/project/rarfile/) library to support searching inside `.rar` files (optional)
|
|
@@ -77,6 +77,8 @@ def run_search_process(file, directory, content, search_instance):
|
|
|
77
77
|
# Archive options
|
|
78
78
|
@click.option('--archive', is_flag=True,
|
|
79
79
|
help='Enable search within archive files (e.g. zip, rar, 7z, gz, bz2, xz, tar, tar.gz, tar.bz2, tar.xz)')
|
|
80
|
+
@click.option('--depth', type=click.IntRange(min=0), show_default=True,
|
|
81
|
+
help='Maximum archive depth to recurse into (e.g. 2 means only 2 levels).')
|
|
80
82
|
@click.option('--arc-ext', multiple=True, type=click.STRING,
|
|
81
83
|
help='Include files with these extensions inside archive files. Example: --arc-ext py --arc-ext js')
|
|
82
84
|
@click.option('--arc-ee', multiple=True, type=click.STRING,
|
|
@@ -94,8 +96,8 @@ def run_search_process(file, directory, content, search_instance):
|
|
|
94
96
|
@click.option('--full-path', is_flag=True, help='Display full paths for results.')
|
|
95
97
|
@click.option('--no-content', is_flag=True, help='Only display files path for content search.')
|
|
96
98
|
def search(query, path, file, directory, content, case_sensitive, ext, exclude_ext, regex, include, exclude,
|
|
97
|
-
re_include, re_exclude, word, expr, timeout, fuzzy, fuzzy_level, max_size, min_size, archive,
|
|
98
|
-
arc_ee, arc_inc, arc_exc, arc_max, arc_min, rarfb, full_path, no_content):
|
|
99
|
+
re_include, re_exclude, word, expr, timeout, fuzzy, fuzzy_level, max_size, min_size, archive, depth,
|
|
100
|
+
arc_ext, arc_ee, arc_inc, arc_exc, arc_max, arc_min, rarfb, full_path, no_content):
|
|
99
101
|
"""Search for files, directories, and file content based on the query."""
|
|
100
102
|
|
|
101
103
|
check_rar_backend(archive, rarfb, query)
|
|
@@ -140,6 +142,7 @@ def search(query, path, file, directory, content, case_sensitive, ext, exclude_e
|
|
|
140
142
|
max_size=max_size,
|
|
141
143
|
min_size=min_size,
|
|
142
144
|
archive=archive,
|
|
145
|
+
depth=depth,
|
|
143
146
|
arc_ext=arc_ext,
|
|
144
147
|
arc_ee=arc_ee,
|
|
145
148
|
arc_inc=arc_inc,
|
|
@@ -1,19 +1,13 @@
|
|
|
1
|
-
import mmap
|
|
2
|
-
import click
|
|
1
|
+
import mmap, click, io
|
|
3
2
|
from pathlib import Path
|
|
4
|
-
from .utils import compile_regex, get_archive_path_size, try_decode
|
|
3
|
+
from .utils import compile_regex, get_archive_path_size, try_decode, get_path_suffix
|
|
5
4
|
from .parser import parse_query_expression, TermNode, highlight_text
|
|
6
5
|
from concurrent.futures import ThreadPoolExecutor
|
|
7
|
-
|
|
8
|
-
import py7zr
|
|
9
|
-
import tarfile
|
|
10
|
-
import gzip
|
|
11
|
-
import bz2
|
|
12
|
-
import lzma
|
|
13
|
-
import rarfile
|
|
6
|
+
# Archive modules
|
|
7
|
+
import zipfile, py7zr, tarfile, gzip, bz2, lzma, rarfile
|
|
14
8
|
|
|
15
9
|
# Archive extensions that are allowed
|
|
16
|
-
ARCHIVE_EXTS = ('zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz', '
|
|
10
|
+
ARCHIVE_EXTS = ('zip', 'rar', '7z', 'tar', 'tar.gz', 'tar.bz2', 'tar.xz', 'gz', 'bz2', 'xz')
|
|
17
11
|
|
|
18
12
|
# Extensions that are not suitable for content search (binary, media, etc.)
|
|
19
13
|
EXCLUDED_EXTENSIONS = (
|
|
@@ -30,7 +24,7 @@ EXCLUDED_EXTENSIONS = (
|
|
|
30
24
|
|
|
31
25
|
class Search:
|
|
32
26
|
def __init__(self, base_path, query, case_sensitive, ext, exclude_ext, regex, include, exclude, re_include,
|
|
33
|
-
re_exclude, whole_word, expr, fuzzy, fuzzy_level, max_size, min_size, archive, arc_ext, arc_ee,
|
|
27
|
+
re_exclude, whole_word, expr, fuzzy, fuzzy_level, max_size, min_size, archive, depth, arc_ext, arc_ee,
|
|
34
28
|
arc_inc, arc_exc, arc_max, arc_min, full_path, no_content):
|
|
35
29
|
"""Initialize search parameters"""
|
|
36
30
|
self.base_path = Path(base_path)
|
|
@@ -50,6 +44,7 @@ class Search:
|
|
|
50
44
|
self.max_size = max_size
|
|
51
45
|
self.min_size = min_size
|
|
52
46
|
self.archive = archive
|
|
47
|
+
self.depth = depth
|
|
53
48
|
self.arc_ext = set(arc_ext)
|
|
54
49
|
self.arc_ee = set(arc_ee) | {''} if arc_ee else set()
|
|
55
50
|
self.arc_inc = {Path(p) for p in arc_inc}
|
|
@@ -71,10 +66,10 @@ class Search:
|
|
|
71
66
|
# If path is inaccessible, skip it.
|
|
72
67
|
return True
|
|
73
68
|
|
|
74
|
-
file_ext =
|
|
69
|
+
file_ext = get_path_suffix(p_resolved)
|
|
75
70
|
|
|
76
71
|
# Ignore some filters for archive files when archive is enabled
|
|
77
|
-
if (not self.archive or not file_ext in
|
|
72
|
+
if (not self.archive or not file_ext in ARCHIVE_EXTS[:-3]) and \
|
|
78
73
|
((search_type in ('file', 'content') and not p_resolved.is_file())
|
|
79
74
|
or (search_type == 'directory' and not p_resolved.is_dir())):
|
|
80
75
|
return True
|
|
@@ -96,11 +91,10 @@ class Search:
|
|
|
96
91
|
|
|
97
92
|
return False
|
|
98
93
|
|
|
99
|
-
|
|
100
94
|
def archive_should_skip(self, path_info: Path, search_type: str, is_file: bool, is_dir: bool, p_size: float):
|
|
101
95
|
"""Check whether the file/directory inside archive files should be skipped based on various filters"""
|
|
102
96
|
|
|
103
|
-
file_ext =
|
|
97
|
+
file_ext = get_path_suffix(path_info)
|
|
104
98
|
|
|
105
99
|
if (search_type in ('file', 'content') and not is_file) \
|
|
106
100
|
or (search_type == 'directory' and not is_dir):
|
|
@@ -122,38 +116,62 @@ class Search:
|
|
|
122
116
|
|
|
123
117
|
return False
|
|
124
118
|
|
|
125
|
-
def extract_names_from_archive(self, file_path: Path, search_type: str
|
|
126
|
-
|
|
119
|
+
def extract_names_from_archive(self, file_path: Path, search_type: str,
|
|
120
|
+
file_bytes: bytes = None, parent_label: str = '',
|
|
121
|
+
depth: int = None):
|
|
122
|
+
"""
|
|
123
|
+
Recursively extract files and directories name from archive files.
|
|
124
|
+
Supports nested archives like a.zip::b.7z::c.txt.
|
|
125
|
+
|
|
126
|
+
Parameters:
|
|
127
|
+
file_path (Path): the archive file path
|
|
128
|
+
search_type (str): search type ( file / directory )
|
|
129
|
+
file_bytes (bytes | None): optional byte data if already read (for recursion)
|
|
130
|
+
parent_label (str): string for nested archive tracking like a.zip::b.7z::file.txt
|
|
131
|
+
depth (int): the depth value that is returned recursively
|
|
132
|
+
|
|
133
|
+
Yields:
|
|
134
|
+
(str, Path): tuple of parent label and file or directory name
|
|
135
|
+
"""
|
|
127
136
|
|
|
128
|
-
file_ext =
|
|
137
|
+
file_ext = get_path_suffix(file_path)
|
|
138
|
+
label_prefix = (parent_label + str(file_path) + '::') if parent_label else '::'
|
|
139
|
+
if depth is None:
|
|
140
|
+
depth = self.depth
|
|
129
141
|
|
|
130
142
|
try:
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
143
|
+
# Decide the stream source: from disk or memory
|
|
144
|
+
file_stream = io.BytesIO(file_bytes) if file_bytes is not None else open(file_path, 'rb')
|
|
145
|
+
|
|
146
|
+
# Handle ZIP and RAR archives
|
|
147
|
+
if file_ext in ('zip', 'rar'):
|
|
148
|
+
opener = {'zip': zipfile.ZipFile, 'rar': rarfile.RarFile}[file_ext]
|
|
149
|
+
with opener(file_stream) as f:
|
|
150
|
+
for info in f.infolist():
|
|
134
151
|
name = Path(info.filename)
|
|
135
152
|
if not self.archive_should_skip(
|
|
136
153
|
name,
|
|
137
154
|
search_type,
|
|
138
155
|
not info.is_dir(),
|
|
139
156
|
info.is_dir(),
|
|
140
|
-
get_archive_path_size(info,
|
|
157
|
+
get_archive_path_size(info, file_ext)
|
|
141
158
|
):
|
|
142
|
-
yield name
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if
|
|
159
|
+
yield label_prefix, name
|
|
160
|
+
|
|
161
|
+
# At each recursion, subtract 1 from depth if it's set
|
|
162
|
+
new_depth = None if depth is None else depth - 1
|
|
163
|
+
# Check if this is a nested archive
|
|
164
|
+
if get_path_suffix(name) in ARCHIVE_EXTS[:-3] and (new_depth is None or new_depth >= 0):
|
|
165
|
+
yield from self.extract_names_from_archive(
|
|
148
166
|
name,
|
|
149
167
|
search_type,
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
168
|
+
f.read(info),
|
|
169
|
+
label_prefix,
|
|
170
|
+
new_depth
|
|
171
|
+
)
|
|
172
|
+
# Handle 7Z archives
|
|
155
173
|
elif file_ext == '7z':
|
|
156
|
-
with py7zr.SevenZipFile(
|
|
174
|
+
with py7zr.SevenZipFile(file_stream, mode='r') as z:
|
|
157
175
|
for info in z.list():
|
|
158
176
|
name = Path(info.filename)
|
|
159
177
|
if not self.archive_should_skip(
|
|
@@ -163,18 +181,32 @@ class Search:
|
|
|
163
181
|
info.is_directory,
|
|
164
182
|
get_archive_path_size(info, '7z')
|
|
165
183
|
):
|
|
166
|
-
yield name
|
|
184
|
+
yield label_prefix, name
|
|
185
|
+
|
|
186
|
+
new_depth = None if depth is None else depth - 1
|
|
187
|
+
if get_path_suffix(name) in ARCHIVE_EXTS[:-3] and (new_depth is None or new_depth >= 0):
|
|
188
|
+
file_data = z.read([info.filename]).get(info.filename)
|
|
189
|
+
if file_data is None:
|
|
190
|
+
continue
|
|
191
|
+
|
|
192
|
+
yield from self.extract_names_from_archive(
|
|
193
|
+
name,
|
|
194
|
+
search_type,
|
|
195
|
+
file_data.read(),
|
|
196
|
+
label_prefix,
|
|
197
|
+
new_depth
|
|
198
|
+
)
|
|
199
|
+
# Handle TAR and compressed TAR formats
|
|
167
200
|
elif file_ext in ('tar', 'tar.gz', 'tar.bz2', 'tar.xz'):
|
|
168
201
|
# Specify the mode based on the file ext to open it
|
|
169
|
-
mode =
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
with tarfile.open(file_path, mode) as tf:
|
|
202
|
+
mode = {
|
|
203
|
+
'tar': 'r',
|
|
204
|
+
'tar.gz': 'r:gz',
|
|
205
|
+
'tar.bz2': 'r:bz2',
|
|
206
|
+
'tar.xz': 'r:xz'
|
|
207
|
+
}[file_ext]
|
|
208
|
+
|
|
209
|
+
with tarfile.open(fileobj=file_stream, mode=mode) as tf:
|
|
178
210
|
for member in tf.getmembers():
|
|
179
211
|
name = Path(member.name)
|
|
180
212
|
if not self.archive_should_skip(
|
|
@@ -184,120 +216,144 @@ class Search:
|
|
|
184
216
|
member.isdir(),
|
|
185
217
|
get_archive_path_size(member, file_ext)
|
|
186
218
|
):
|
|
187
|
-
yield name
|
|
219
|
+
yield label_prefix, name
|
|
220
|
+
|
|
221
|
+
new_depth = None if depth is None else depth - 1
|
|
222
|
+
if get_path_suffix(name) in ARCHIVE_EXTS[:-3] and (new_depth is None or new_depth >= 0):
|
|
223
|
+
f = tf.extractfile(member)
|
|
224
|
+
if f is None:
|
|
225
|
+
continue
|
|
226
|
+
|
|
227
|
+
yield from self.extract_names_from_archive(
|
|
228
|
+
name,
|
|
229
|
+
search_type,
|
|
230
|
+
f.read(),
|
|
231
|
+
label_prefix,
|
|
232
|
+
new_depth
|
|
233
|
+
)
|
|
188
234
|
except Exception:
|
|
189
235
|
return # silently skip invalid archives
|
|
190
236
|
|
|
191
|
-
def extract_text_from_archive(self, file_path: Path
|
|
237
|
+
def extract_text_from_archive(self, file_path: Path, file_bytes: bytes = None,
|
|
238
|
+
parent_label: str = '', depth: int = None):
|
|
192
239
|
"""
|
|
193
|
-
|
|
194
|
-
Supports
|
|
240
|
+
Recursively extract (path_label, text_content) from any archive file.
|
|
241
|
+
Supports nested archives like a.zip::b.7z::c.txt.
|
|
242
|
+
|
|
243
|
+
Parameters:
|
|
244
|
+
file_path (Path): the archive file path
|
|
245
|
+
file_bytes (bytes | None): optional byte data if already read (for recursion)
|
|
246
|
+
parent_label (str): string for nested archive tracking like a.zip::b.7z::file.txt
|
|
247
|
+
depth (int): the depth value that is returned recursively
|
|
248
|
+
|
|
249
|
+
Yields:
|
|
250
|
+
(str, str): tuple of full virtual path and decoded content text
|
|
195
251
|
"""
|
|
196
252
|
|
|
197
|
-
file_ext =
|
|
253
|
+
file_ext = get_path_suffix(file_path)
|
|
254
|
+
label_prefix = (parent_label + str(file_path) + '::') if parent_label else '::'
|
|
255
|
+
if depth is None:
|
|
256
|
+
depth = self.depth
|
|
198
257
|
|
|
199
258
|
try:
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
info.is_dir(),
|
|
209
|
-
get_archive_path_size(info, 'zip')
|
|
210
|
-
):
|
|
211
|
-
continue
|
|
212
|
-
|
|
213
|
-
with zf.open(info) as f:
|
|
214
|
-
data = f.read()
|
|
215
|
-
text = try_decode(data)
|
|
216
|
-
if text is not None:
|
|
217
|
-
yield info.filename, text
|
|
218
|
-
elif file_ext == 'rar':
|
|
219
|
-
with rarfile.RarFile(file_path) as rf:
|
|
220
|
-
for info in rf.infolist():
|
|
259
|
+
# Decide the stream source: from disk or memory
|
|
260
|
+
file_stream = io.BytesIO(file_bytes) if file_bytes is not None else open(file_path, 'rb')
|
|
261
|
+
|
|
262
|
+
# Handle ZIP and RAR archives
|
|
263
|
+
if file_ext in ('zip', 'rar'):
|
|
264
|
+
opener = {'zip': zipfile.ZipFile, 'rar': rarfile.RarFile}[file_ext]
|
|
265
|
+
with opener(file_stream) as f:
|
|
266
|
+
for info in f.infolist():
|
|
221
267
|
file_name = Path(info.filename)
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
268
|
+
data = f.read(info)
|
|
269
|
+
# At each recursion, subtract 1 from depth if it's set
|
|
270
|
+
new_depth = None if depth is None else depth - 1
|
|
271
|
+
|
|
272
|
+
# Check if this is a nested archive
|
|
273
|
+
if get_path_suffix(file_name) in ARCHIVE_EXTS and (new_depth is None or new_depth >= 0):
|
|
274
|
+
yield from self.extract_text_from_archive(file_name, data, label_prefix, new_depth)
|
|
275
|
+
else:
|
|
276
|
+
if self.archive_should_skip(
|
|
277
|
+
file_name,
|
|
278
|
+
'content',
|
|
279
|
+
not info.is_dir(),
|
|
280
|
+
info.is_dir(),
|
|
281
|
+
get_archive_path_size(info, file_ext)
|
|
282
|
+
):
|
|
283
|
+
continue
|
|
230
284
|
|
|
231
|
-
with rf.open(info) as f:
|
|
232
|
-
data = f.read()
|
|
233
285
|
text = try_decode(data)
|
|
234
286
|
if text is not None:
|
|
235
|
-
yield
|
|
287
|
+
yield label_prefix + str(file_name), text
|
|
288
|
+
# Handle 7Z archives
|
|
236
289
|
elif file_ext == '7z':
|
|
237
|
-
with py7zr.SevenZipFile(
|
|
290
|
+
with py7zr.SevenZipFile(file_stream, mode='r') as archive:
|
|
238
291
|
for info in archive.list():
|
|
239
|
-
|
|
240
|
-
if
|
|
241
|
-
file_name,
|
|
242
|
-
'content',
|
|
243
|
-
not info.is_directory,
|
|
244
|
-
info.is_directory,
|
|
245
|
-
get_archive_path_size(info, '7z')
|
|
246
|
-
):
|
|
292
|
+
file_data = archive.read([info.filename]).get(info.filename)
|
|
293
|
+
if file_data is None:
|
|
247
294
|
continue
|
|
248
295
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
296
|
+
file_name = Path(info.filename)
|
|
297
|
+
data = file_data.read()
|
|
298
|
+
new_depth = None if depth is None else depth - 1
|
|
299
|
+
|
|
300
|
+
if get_path_suffix(file_name) in ARCHIVE_EXTS and (new_depth is None or new_depth >= 0):
|
|
301
|
+
yield from self.extract_text_from_archive(file_name, data, label_prefix, new_depth)
|
|
302
|
+
else:
|
|
303
|
+
if self.archive_should_skip(
|
|
304
|
+
file_name,
|
|
305
|
+
'content',
|
|
306
|
+
not info.is_directory,
|
|
307
|
+
info.is_directory,
|
|
308
|
+
get_archive_path_size(info, '7z')
|
|
309
|
+
):
|
|
310
|
+
continue
|
|
311
|
+
|
|
312
|
+
text = try_decode(data)
|
|
313
|
+
if text is not None:
|
|
314
|
+
yield label_prefix + str(file_name), text
|
|
315
|
+
# Handle TAR and compressed TAR formats
|
|
256
316
|
elif file_ext in ('tar', 'tar.gz', 'tar.bz2', 'tar.xz'):
|
|
257
|
-
mode =
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
with tarfile.open(
|
|
317
|
+
mode = {
|
|
318
|
+
'tar': 'r',
|
|
319
|
+
'tar.gz': 'r:gz',
|
|
320
|
+
'tar.bz2': 'r:bz2',
|
|
321
|
+
'tar.xz': 'r:xz'
|
|
322
|
+
}[file_ext]
|
|
323
|
+
|
|
324
|
+
with tarfile.open(fileobj=file_stream, mode=mode) as tf:
|
|
265
325
|
for member in tf.getmembers():
|
|
266
|
-
file_name = Path(member.name)
|
|
267
|
-
if self.archive_should_skip(
|
|
268
|
-
file_name,
|
|
269
|
-
'content',
|
|
270
|
-
member.isfile(),
|
|
271
|
-
member.isdir(),
|
|
272
|
-
get_archive_path_size(member, file_ext)
|
|
273
|
-
):
|
|
274
|
-
continue
|
|
275
|
-
|
|
276
326
|
f = tf.extractfile(member)
|
|
277
327
|
if f is None:
|
|
278
328
|
continue
|
|
329
|
+
|
|
330
|
+
file_name = Path(member.name)
|
|
279
331
|
data = f.read()
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
332
|
+
new_depth = None if depth is None else depth - 1
|
|
333
|
+
|
|
334
|
+
if get_path_suffix(file_name) in ARCHIVE_EXTS and (new_depth is None or new_depth >= 0):
|
|
335
|
+
yield from self.extract_text_from_archive(file_name, data, label_prefix, new_depth)
|
|
336
|
+
else:
|
|
337
|
+
if self.archive_should_skip(
|
|
338
|
+
file_name,
|
|
339
|
+
'content',
|
|
340
|
+
member.isfile(),
|
|
341
|
+
member.isdir(),
|
|
342
|
+
get_archive_path_size(member, file_ext)
|
|
343
|
+
):
|
|
344
|
+
continue
|
|
345
|
+
|
|
346
|
+
text = try_decode(data)
|
|
347
|
+
if text is not None:
|
|
348
|
+
yield label_prefix + str(file_name), text
|
|
349
|
+
# Handle single compressed files like .gz, .bz2, .xz
|
|
350
|
+
elif file_ext in ARCHIVE_EXTS[-3:]:
|
|
351
|
+
opener = {'gz': gzip.open, 'bz2': bz2.open, 'xz': lzma.open}[file_ext]
|
|
352
|
+
with opener(file_stream, 'rb') as f:
|
|
297
353
|
data = f.read()
|
|
298
354
|
text = try_decode(data)
|
|
299
355
|
if text is not None:
|
|
300
|
-
yield
|
|
356
|
+
yield file_ext, text
|
|
301
357
|
except Exception:
|
|
302
358
|
return
|
|
303
359
|
|
|
@@ -319,7 +375,7 @@ class Search:
|
|
|
319
375
|
|
|
320
376
|
# Choose parent path based on full_path flag
|
|
321
377
|
p_parent = p_resolved.parent if self.full_path else p.parent
|
|
322
|
-
p_ext =
|
|
378
|
+
p_ext = get_path_suffix(p_resolved)
|
|
323
379
|
|
|
324
380
|
if pattern.evaluate(p.name) and not (search_type == 'directory' and p_resolved.is_file()):
|
|
325
381
|
# Highlight matched query in the name
|
|
@@ -327,11 +383,11 @@ class Search:
|
|
|
327
383
|
matches.append(f'{p_parent}\\{highlighted_name}')
|
|
328
384
|
|
|
329
385
|
# Search for files and directories name inside archive files if archive is active
|
|
330
|
-
if self.archive and p_ext in
|
|
331
|
-
for name in self.extract_names_from_archive(p_resolved, search_type):
|
|
386
|
+
if self.archive and p_ext in ARCHIVE_EXTS[:-3]:
|
|
387
|
+
for label, name in self.extract_names_from_archive(p_resolved, search_type):
|
|
332
388
|
if pattern.evaluate(name.name):
|
|
333
389
|
highlighted_name = highlight_text(pattern, name.name, self.fuzzy)
|
|
334
|
-
matches.append(f'{p_parent}\\{p.name}
|
|
390
|
+
matches.append(f'{p_parent}\\{p.name}{label}{name.parent}\\{highlighted_name}')
|
|
335
391
|
else: # content search
|
|
336
392
|
# Use dictionary: key: file path (colored), value: list of line matches
|
|
337
393
|
matches = {} if not self.no_content else set()
|
|
@@ -355,13 +411,14 @@ class Search:
|
|
|
355
411
|
file_label = str(file_path.resolve()) if self.full_path else str(file_path)
|
|
356
412
|
|
|
357
413
|
# First, check if the file is an archive, extract it from the archive and perform a search
|
|
358
|
-
if self.archive and
|
|
414
|
+
if self.archive and get_path_suffix(file_path) in ARCHIVE_EXTS:
|
|
359
415
|
for fname, content in self.extract_text_from_archive(file_path):
|
|
360
416
|
if not pattern.evaluate(content) and not self.expr:
|
|
361
417
|
continue
|
|
362
418
|
|
|
363
|
-
# Change file_label for archive files
|
|
364
|
-
|
|
419
|
+
# Change file_label for archive files (zip, rar, 7z, tar)
|
|
420
|
+
if fname not in ARCHIVE_EXTS[-3:]:
|
|
421
|
+
file_label += fname
|
|
365
422
|
|
|
366
423
|
if self.no_content and not self.expr:
|
|
367
424
|
matches.add(click.style(file_label, fg='cyan'))
|
|
@@ -378,7 +435,7 @@ class Search:
|
|
|
378
435
|
|
|
379
436
|
count = pattern.count_matches(line) if isinstance(pattern, TermNode) else 0
|
|
380
437
|
# Highlight the matching parts in green
|
|
381
|
-
highlighted = highlight_text(pattern, line, self.fuzzy)
|
|
438
|
+
highlighted = highlight_text(pattern, line.strip(), self.fuzzy)
|
|
382
439
|
# Show a note if the pattern repeats 3 or more times
|
|
383
440
|
count_query = f' - Repeated {count} times' if count >= 3 else ''
|
|
384
441
|
# Format the output line with line number and highlighted matches
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pseek
|
|
3
|
-
Version: 2.5.
|
|
3
|
+
Version: 2.5.3
|
|
4
4
|
Summary: Pseek is a Python library to search files, folders, and text
|
|
5
5
|
Author-email: Arian <ariannasiri86@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -102,6 +102,7 @@ pseek "error\d+" --regex
|
|
|
102
102
|
| `--fuzzy_level` | Similarity threshold from 0 to 99 for fuzzy search (default: `80`) |
|
|
103
103
|
| `--max-size`, `--min-size` | Specify maximum and minimum sizes for files and directories |
|
|
104
104
|
| `--archive` | Enable search within archive files (e.g. `zip`, `rar`, `7z`, `gz`, `bz2`, `xz`, `tar`, `tar.gz`, `tar.bz2`, `tar.xz`) |
|
|
105
|
+
| `--depth` | Maximum archive depth to recurse into (e.g. 2 means only 2 levels) |
|
|
105
106
|
| `--arc-ext`, `--arc-ee` | Filter by file extension inside archive files |
|
|
106
107
|
| `--arc-inc`, `--arc-exc` | Limit search results to specific set of directories or files inside archive files |
|
|
107
108
|
| `--arc-max`, `--arc-min` | Specify maximum and minimum sizes for files inside archive files (It doesn't work for directories because their size is zero in archive files) |
|
|
@@ -112,4 +113,4 @@ pseek "error\d+" --regex
|
|
|
112
113
|
## Requirements
|
|
113
114
|
|
|
114
115
|
* Python 3.6 or higher
|
|
115
|
-
* `unrar`, `bsdtar`, `unar` or `7zip` for the rarfile library to support searching inside `.rar` files (optional)
|
|
116
|
+
* `unrar`, `bsdtar`, `unar` or `7zip` for the [rarfile](https://pypi.org/project/rarfile/) library to support searching inside `.rar` files (optional)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|