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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pseek
3
- Version: 2.5.1
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, arc_ext,
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
- import zipfile
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', 'tar.gz', 'tar.bz2', 'tar.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 = ''.join(p_resolved.suffixes)[1:].lower()
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 ('zip', 'rar', '7z', 'tar', 'tar.gz', 'tar.bz2', 'tar.xz')) and \
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 = ''.join(path_info.suffixes)[1:].lower()
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
- """Extract files and directories name from archive files to search"""
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 = ''.join(file_path.suffixes)[1:].lower()
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
- if file_ext == 'zip':
132
- with zipfile.ZipFile(file_path) as zf:
133
- for info in zf.infolist():
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, 'zip')
157
+ get_archive_path_size(info, file_ext)
141
158
  ):
142
- yield name
143
- elif file_ext == 'rar':
144
- with rarfile.RarFile(file_path) as rf:
145
- for info in rf.infolist():
146
- name = Path(info.filename)
147
- if not self.archive_should_skip(
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
- not info.is_dir(),
151
- info.is_dir(),
152
- get_archive_path_size(info, 'rar')
153
- ):
154
- yield name
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(file_path, mode='r') as z:
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 = 'r'
170
- if file_ext == 'tar.gz':
171
- mode = 'r:gz'
172
- elif file_ext == 'tar.bz2':
173
- mode = 'r:bz2'
174
- elif file_ext == 'tar.xz':
175
- mode = 'r:xz'
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
- Generator yielding (filename, content_text) from archive files.
194
- Supports zip, rar, tar, gz, bz2, xz, 7z.
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 = ''.join(file_path.suffixes)[1:].lower()
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
- if file_ext == 'zip':
201
- with zipfile.ZipFile(file_path) as zf:
202
- for info in zf.infolist():
203
- file_name = Path(info.filename)
204
- if self.archive_should_skip(
205
- file_name,
206
- 'content',
207
- not info.is_dir(),
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
- if self.archive_should_skip(
223
- file_name,
224
- 'content',
225
- not info.is_dir(),
226
- info.is_dir(),
227
- get_archive_path_size(info, 'rar')
228
- ):
229
- continue
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 info.filename, text
287
+ yield label_prefix + str(file_name), text
288
+ # Handle 7Z archives
236
289
  elif file_ext == '7z':
237
- with py7zr.SevenZipFile(file_path, mode='r') as archive:
290
+ with py7zr.SevenZipFile(file_stream, mode='r') as archive:
238
291
  for info in archive.list():
239
- file_name = Path(info.filename)
240
- if self.archive_should_skip(
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
- data = archive.read([info.filename])
250
- filedata = data.get(info.filename)
251
- if filedata is None:
252
- continue
253
- text = try_decode(filedata.read())
254
- if text is not None:
255
- yield info.filename, text
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 = 'r'
258
- if file_ext == 'tar.gz':
259
- mode = 'r:gz'
260
- elif file_ext == 'tar.bz2':
261
- mode = 'r:bz2'
262
- elif file_ext == 'tar.xz':
263
- mode = 'r:xz'
264
- with tarfile.open(file_path, mode) as tf:
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
- text = try_decode(data)
281
- if text is not None:
282
- yield member.name, text
283
- elif file_ext == 'gz':
284
- with gzip.open(file_path, 'rb') as f:
285
- data = f.read()
286
- text = try_decode(data)
287
- if text is not None:
288
- yield file_path.name, text
289
- elif file_ext == 'bz2':
290
- with bz2.open(file_path, 'rb') as f:
291
- data = f.read()
292
- text = try_decode(data)
293
- if text is not None:
294
- yield file_path.name, text
295
- elif file_ext == 'xz':
296
- with lzma.open(file_path, 'rb') as f:
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 file_path.name, text
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 = ''.join(p_resolved.suffixes)[1:].lower()
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 ('zip', 'rar', '7z', 'tar', 'tar.gz', 'tar.bz2', 'tar.xz'):
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}::{name.parent}\\{highlighted_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 any(str(file_path).endswith(ext) for ext in ARCHIVE_EXTS):
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
- file_label += '::' + fname.replace('/', '\\')
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
@@ -85,3 +85,7 @@ def check_rar_backend(archive_enabled: bool, tool_path: str, backend: str):
85
85
  rarfile.UNAR_TOOL = tool
86
86
  elif b == '7z':
87
87
  rarfile.SEVENZIP_TOOL = tool
88
+
89
+
90
+ def get_path_suffix(path: Path) -> str:
91
+ return ''.join(path.suffixes)[1:].lower()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pseek
3
- Version: 2.5.1
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)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pseek"
7
- version = "2.5.1"
7
+ version = "2.5.3"
8
8
  description = "Pseek is a Python library to search files, folders, and text"
9
9
  readme = "README.md"
10
10
  license = "MIT"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes