pseek 3.5.0__tar.gz → 3.6.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.
- {pseek-3.5.0 → pseek-3.6.0}/PKG-INFO +26 -1
- {pseek-3.5.0 → pseek-3.6.0}/README.md +25 -0
- {pseek-3.5.0 → pseek-3.6.0}/pseek/archive.py +11 -2
- {pseek-3.5.0 → pseek-3.6.0}/pseek/cli.py +14 -5
- {pseek-3.5.0 → pseek-3.6.0}/pseek/parser.py +6 -2
- {pseek-3.5.0 → pseek-3.6.0}/pseek/searcher.py +222 -82
- {pseek-3.5.0 → pseek-3.6.0}/pseek/structs.py +7 -3
- {pseek-3.5.0 → pseek-3.6.0}/pseek/utils.py +3 -1
- {pseek-3.5.0 → pseek-3.6.0}/pyproject.toml +1 -1
- {pseek-3.5.0 → pseek-3.6.0}/.gitattributes +0 -0
- {pseek-3.5.0 → pseek-3.6.0}/.github/workflows/publish.yml +0 -0
- {pseek-3.5.0 → pseek-3.6.0}/.gitignore +0 -0
- {pseek-3.5.0 → pseek-3.6.0}/Cargo.lock +0 -0
- {pseek-3.5.0 → pseek-3.6.0}/Cargo.toml +0 -0
- {pseek-3.5.0 → pseek-3.6.0}/LICENSE +0 -0
- {pseek-3.5.0 → pseek-3.6.0}/pseek/__init__.py +0 -0
- {pseek-3.5.0 → pseek-3.6.0}/pseek/compound_extensions +0 -0
- {pseek-3.5.0 → pseek-3.6.0}/scripts/check_version.py +0 -0
- {pseek-3.5.0 → pseek-3.6.0}/src/lib.rs +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pseek
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.6.0
|
|
4
4
|
Classifier: Development Status :: 5 - Production/Stable
|
|
5
5
|
Classifier: Environment :: Console
|
|
6
6
|
Classifier: Intended Audience :: Developers
|
|
@@ -75,6 +75,7 @@ Fast and powerful command-line search tool for finding files, directories, and t
|
|
|
75
75
|
* [Exclude](#exclude)
|
|
76
76
|
* [Size](#size-filtering)
|
|
77
77
|
* [Depth](#depth)
|
|
78
|
+
* [Parallel Search](#parallel-search)
|
|
78
79
|
* [Archive Search](#archive-search)
|
|
79
80
|
* [Nested Archives](#nested-archives)
|
|
80
81
|
* [Archive Filters](#archive-filters)
|
|
@@ -236,6 +237,7 @@ psk --exclude build
|
|
|
236
237
|
| `--absolute-path` | Display full path of files and directories |
|
|
237
238
|
| `--paths-only` | Only show matching file paths for content search |
|
|
238
239
|
| `--stats` | Show search statistics including result counts and search time |
|
|
240
|
+
| `--threads` | This flag sets the approximate number of threads to use. A value of 0 (which is the default) causes pseek to choose the thread count using heuristics |
|
|
239
241
|
|
|
240
242
|
## Specifying the Root Directory
|
|
241
243
|
|
|
@@ -720,6 +722,29 @@ project/ ← base directory
|
|
|
720
722
|
|
|
721
723
|
If `--depth` is not specified, the search has no depth limit.
|
|
722
724
|
|
|
725
|
+
## Parallel Search
|
|
726
|
+
|
|
727
|
+
Pseek can process multiple file paths concurrently to improve search performance, especially when searching large numbers of files.
|
|
728
|
+
|
|
729
|
+
Use `--threads` to control the maximum number of worker threads:
|
|
730
|
+
|
|
731
|
+
```bash
|
|
732
|
+
# Use the default number of worker threads
|
|
733
|
+
psk TODO
|
|
734
|
+
|
|
735
|
+
# Use 4 worker threads
|
|
736
|
+
psk TODO --threads 4
|
|
737
|
+
|
|
738
|
+
# Use a single worker thread
|
|
739
|
+
psk TODO --threads 1
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
If `--threads` is not specified, Pseek automatically selects the number of workers based on the available CPU count. Setting `--threads 1` disables parallel processing of candidate paths.
|
|
743
|
+
|
|
744
|
+
More threads do not always mean faster searches. Performance depends on factors such as file sizes, storage speed, and the type of search being performed. You can experiment with different values to find the best setting for your workload.
|
|
745
|
+
|
|
746
|
+
> **Note:** Parallel processing may change the order in which results are produced. Search filters and matching rules remain unchanged.
|
|
747
|
+
|
|
723
748
|
## Archive Search
|
|
724
749
|
|
|
725
750
|
Enable archive support:
|
|
@@ -47,6 +47,7 @@ Fast and powerful command-line search tool for finding files, directories, and t
|
|
|
47
47
|
* [Exclude](#exclude)
|
|
48
48
|
* [Size](#size-filtering)
|
|
49
49
|
* [Depth](#depth)
|
|
50
|
+
* [Parallel Search](#parallel-search)
|
|
50
51
|
* [Archive Search](#archive-search)
|
|
51
52
|
* [Nested Archives](#nested-archives)
|
|
52
53
|
* [Archive Filters](#archive-filters)
|
|
@@ -208,6 +209,7 @@ psk --exclude build
|
|
|
208
209
|
| `--absolute-path` | Display full path of files and directories |
|
|
209
210
|
| `--paths-only` | Only show matching file paths for content search |
|
|
210
211
|
| `--stats` | Show search statistics including result counts and search time |
|
|
212
|
+
| `--threads` | This flag sets the approximate number of threads to use. A value of 0 (which is the default) causes pseek to choose the thread count using heuristics |
|
|
211
213
|
|
|
212
214
|
## Specifying the Root Directory
|
|
213
215
|
|
|
@@ -692,6 +694,29 @@ project/ ← base directory
|
|
|
692
694
|
|
|
693
695
|
If `--depth` is not specified, the search has no depth limit.
|
|
694
696
|
|
|
697
|
+
## Parallel Search
|
|
698
|
+
|
|
699
|
+
Pseek can process multiple file paths concurrently to improve search performance, especially when searching large numbers of files.
|
|
700
|
+
|
|
701
|
+
Use `--threads` to control the maximum number of worker threads:
|
|
702
|
+
|
|
703
|
+
```bash
|
|
704
|
+
# Use the default number of worker threads
|
|
705
|
+
psk TODO
|
|
706
|
+
|
|
707
|
+
# Use 4 worker threads
|
|
708
|
+
psk TODO --threads 4
|
|
709
|
+
|
|
710
|
+
# Use a single worker thread
|
|
711
|
+
psk TODO --threads 1
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
If `--threads` is not specified, Pseek automatically selects the number of workers based on the available CPU count. Setting `--threads 1` disables parallel processing of candidate paths.
|
|
715
|
+
|
|
716
|
+
More threads do not always mean faster searches. Performance depends on factors such as file sizes, storage speed, and the type of search being performed. You can experiment with different values to find the best setting for your workload.
|
|
717
|
+
|
|
718
|
+
> **Note:** Parallel processing may change the order in which results are produced. Search filters and matching rules remain unchanged.
|
|
719
|
+
|
|
695
720
|
## Archive Search
|
|
696
721
|
|
|
697
722
|
Enable archive support:
|
|
@@ -1,8 +1,17 @@
|
|
|
1
|
+
import bz2
|
|
2
|
+
import gzip
|
|
1
3
|
import io
|
|
4
|
+
import lzma
|
|
5
|
+
import tarfile
|
|
6
|
+
|
|
7
|
+
# Archive modules
|
|
8
|
+
import zipfile
|
|
2
9
|
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import py7zr
|
|
12
|
+
import rarfile
|
|
13
|
+
|
|
3
14
|
from .utils import get_path_suffix, is_binary
|
|
4
|
-
# Archive modules
|
|
5
|
-
import zipfile, py7zr, tarfile, gzip, bz2, lzma, rarfile
|
|
6
15
|
|
|
7
16
|
# Archive extensions that are allowed
|
|
8
17
|
ARCHIVE_EXTS = ('zip', 'rar', '7z', 'tar', 'tar.gz', 'tar.bz2', 'tar.xz', 'gz', 'bz2', 'xz')
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
from multiprocessing import Process, Queue
|
|
5
|
-
from queue import Empty
|
|
1
|
+
import platform
|
|
2
|
+
import shutil
|
|
3
|
+
import time
|
|
6
4
|
from collections import defaultdict
|
|
5
|
+
from multiprocessing import Process, Queue
|
|
7
6
|
from pathlib import Path
|
|
7
|
+
from queue import Empty
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
import rarfile
|
|
11
|
+
|
|
12
|
+
from .searcher import seek
|
|
13
|
+
from .structs import SearchConfig
|
|
8
14
|
|
|
9
15
|
BACKEND_PATH = Path(__file__).parent / "RARBackend"
|
|
10
16
|
|
|
@@ -424,6 +430,9 @@ def check_rar_backend(archive_enabled: bool, tool_path: str, backend: str):
|
|
|
424
430
|
@click.option('--timeout', metavar='FLOAT',
|
|
425
431
|
type=click.FloatRange(min=0, min_open=True),
|
|
426
432
|
help='Stop the search after the specified number of seconds.')
|
|
433
|
+
@click.option('--threads', metavar='NUM', type=click.IntRange(min=0), default=0,
|
|
434
|
+
help='This flag sets the approximate number of threads to use. '
|
|
435
|
+
'A value of 0 (which is the default) causes pseek to choose the thread count using heuristics.')
|
|
427
436
|
def search(**kwargs):
|
|
428
437
|
"""Search for files, directories, and file content based on the query."""
|
|
429
438
|
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
import click
|
|
2
5
|
from lark import Lark, Transformer
|
|
3
|
-
from .utils import compile_regex
|
|
4
6
|
from rapidfuzz import fuzz
|
|
5
7
|
|
|
8
|
+
from .utils import compile_regex
|
|
9
|
+
|
|
6
10
|
|
|
7
11
|
class ExprNode:
|
|
8
12
|
"""Base class for expression tree nodes"""
|
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import mmap
|
|
1
|
+
import mmap
|
|
2
|
+
import os
|
|
3
|
+
from collections import defaultdict, deque
|
|
4
|
+
from concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, wait
|
|
2
5
|
from errno import ELOOP
|
|
3
|
-
from
|
|
6
|
+
from itertools import islice
|
|
4
7
|
from pathlib import Path
|
|
5
|
-
from
|
|
6
|
-
|
|
7
|
-
from
|
|
8
|
+
from threading import RLock
|
|
9
|
+
|
|
10
|
+
from pseek import _pignore
|
|
11
|
+
|
|
8
12
|
from .archive import ARCHIVE_EXTS, extract_names_from_archive, extract_text_from_archive
|
|
9
|
-
from .
|
|
13
|
+
from .parser import TermNode, find_matches, parse_query_expression
|
|
14
|
+
from .structs import ContentResult, FileDirResult, Line, MatchGroup
|
|
15
|
+
from .utils import get_path_suffix, is_binary
|
|
10
16
|
|
|
11
17
|
|
|
12
18
|
def should_skip(config, p: Path, file_ext: str, p_size: int) -> bool:
|
|
@@ -14,7 +20,7 @@ def should_skip(config, p: Path, file_ext: str, p_size: int) -> bool:
|
|
|
14
20
|
Check whether the file/directory should be skipped based on various filters.
|
|
15
21
|
Returns True if the path should be skipped.
|
|
16
22
|
"""
|
|
17
|
-
|
|
23
|
+
return (config.ext and file_ext not in config.ext) \
|
|
18
24
|
or (config.exclude_ext and file_ext in config.exclude_ext) \
|
|
19
25
|
or (config.size and (p.is_dir() or not any( # .stat().st_size doesn't give actual size of dir.
|
|
20
26
|
# To measure the actual size,
|
|
@@ -22,35 +28,37 @@ def should_skip(config, p: Path, file_ext: str, p_size: int) -> bool:
|
|
|
22
28
|
# which is time-consuming
|
|
23
29
|
minimum <= p_size <= maximum
|
|
24
30
|
for minimum, maximum in config.size
|
|
25
|
-
)))
|
|
26
|
-
return True
|
|
27
|
-
|
|
28
|
-
# Filter by regex include and exclude
|
|
29
|
-
str_p = str(p)
|
|
30
|
-
if config.re_include:
|
|
31
|
-
return not config.re_include.search(str_p)
|
|
32
|
-
if config.re_exclude:
|
|
33
|
-
return config.re_exclude.search(str_p) is not None
|
|
31
|
+
)))
|
|
34
32
|
|
|
35
|
-
return False
|
|
36
33
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
def add_result(matches, result_queue, match_type, value, lock):
|
|
35
|
+
if result_queue is not None:
|
|
36
|
+
# Queue.put() is thread-safe for standard Python queues
|
|
40
37
|
result_queue.put((match_type, value))
|
|
41
|
-
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
if lock is None:
|
|
42
41
|
matches[match_type].append(value)
|
|
42
|
+
else:
|
|
43
|
+
with lock:
|
|
44
|
+
matches[match_type].append(value)
|
|
43
45
|
|
|
44
46
|
|
|
45
|
-
def add_metric(metrics, result_queue, name, value):
|
|
46
|
-
if result_queue:
|
|
47
|
+
def add_metric(metrics, result_queue, name, value, lock):
|
|
48
|
+
if result_queue is not None:
|
|
49
|
+
# Queue.put() is thread-safe for standard Python queues
|
|
47
50
|
result_queue.put(('metric', (name, value)))
|
|
48
|
-
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
if lock is None:
|
|
49
54
|
metrics[name].add(value)
|
|
55
|
+
else:
|
|
56
|
+
with lock:
|
|
57
|
+
metrics[name].add(value)
|
|
50
58
|
|
|
51
59
|
|
|
52
60
|
def search_file_and_dir(config, matches: dict, pattern, p: Path,
|
|
53
|
-
p_ext: str, metrics, result_queue):
|
|
61
|
+
p_ext: str, metrics, result_queue, result_lock):
|
|
54
62
|
"""Search files and folders on the system and within archive files"""
|
|
55
63
|
|
|
56
64
|
path_str = str(p)
|
|
@@ -78,7 +86,8 @@ def search_file_and_dir(config, matches: dict, pattern, p: Path,
|
|
|
78
86
|
FileDirResult(
|
|
79
87
|
path=path_str,
|
|
80
88
|
matches=name_matches
|
|
81
|
-
)
|
|
89
|
+
),
|
|
90
|
+
result_lock
|
|
82
91
|
)
|
|
83
92
|
|
|
84
93
|
# Search for files and directories name inside archive files if archive is active
|
|
@@ -90,14 +99,32 @@ def search_file_and_dir(config, matches: dict, pattern, p: Path,
|
|
|
90
99
|
full_virtual_path = (path_str, *virtual_path, name_str)
|
|
91
100
|
|
|
92
101
|
if not is_dir and config.file:
|
|
93
|
-
add_metric(
|
|
102
|
+
add_metric(
|
|
103
|
+
metrics,
|
|
104
|
+
result_queue,
|
|
105
|
+
'files_scanned',
|
|
106
|
+
full_virtual_path,
|
|
107
|
+
result_lock
|
|
108
|
+
)
|
|
94
109
|
elif is_dir:
|
|
95
|
-
add_metric(
|
|
110
|
+
add_metric(
|
|
111
|
+
metrics,
|
|
112
|
+
result_queue,
|
|
113
|
+
'directories_scanned',
|
|
114
|
+
full_virtual_path,
|
|
115
|
+
result_lock
|
|
116
|
+
)
|
|
96
117
|
|
|
97
118
|
# We cannot use "full_virtual_path[-1]" because it may be limited by --arc-depth
|
|
98
119
|
# and may not enter the archive at all to scan inside it
|
|
99
120
|
if len(full_virtual_path) >= 3 and get_path_suffix(full_virtual_path[-2]) in ARCHIVE_EXTS[:-3]:
|
|
100
|
-
add_metric(
|
|
121
|
+
add_metric(
|
|
122
|
+
metrics,
|
|
123
|
+
result_queue,
|
|
124
|
+
'archives_scanned',
|
|
125
|
+
full_virtual_path,
|
|
126
|
+
result_lock
|
|
127
|
+
)
|
|
101
128
|
|
|
102
129
|
arc_match_type = (
|
|
103
130
|
'file' if config.file and not is_dir else
|
|
@@ -122,7 +149,8 @@ def search_file_and_dir(config, matches: dict, pattern, p: Path,
|
|
|
122
149
|
path=path_str,
|
|
123
150
|
matches=name_matches,
|
|
124
151
|
virtual_path=[*virtual_path, name_str]
|
|
125
|
-
)
|
|
152
|
+
),
|
|
153
|
+
result_lock
|
|
126
154
|
)
|
|
127
155
|
|
|
128
156
|
|
|
@@ -202,7 +230,7 @@ class ContextCollector:
|
|
|
202
230
|
|
|
203
231
|
|
|
204
232
|
def search_content(config, matches: dict, pattern, binary_pattern,
|
|
205
|
-
p: Path, p_ext: str, metrics, result_queue):
|
|
233
|
+
p: Path, p_ext: str, metrics, result_queue, result_lock):
|
|
206
234
|
"""Search within the contents of system files and files inside archive files"""
|
|
207
235
|
|
|
208
236
|
path_str = str(p)
|
|
@@ -217,12 +245,19 @@ def search_content(config, matches: dict, pattern, binary_pattern,
|
|
|
217
245
|
metrics,
|
|
218
246
|
result_queue,
|
|
219
247
|
'files_scanned',
|
|
220
|
-
full_virtual_path if virtual_path else path_str
|
|
248
|
+
full_virtual_path if virtual_path else path_str,
|
|
249
|
+
result_lock
|
|
221
250
|
)
|
|
222
251
|
|
|
223
252
|
if (len(full_virtual_path) >= 3 and get_path_suffix(full_virtual_path[-2]) in ARCHIVE_EXTS[:-3] \
|
|
224
253
|
or get_path_suffix(full_virtual_path[-1]) in ARCHIVE_EXTS[-3:]):
|
|
225
|
-
add_metric(
|
|
254
|
+
add_metric(
|
|
255
|
+
metrics,
|
|
256
|
+
result_queue,
|
|
257
|
+
'archives_scanned',
|
|
258
|
+
full_virtual_path,
|
|
259
|
+
result_lock
|
|
260
|
+
)
|
|
226
261
|
|
|
227
262
|
if binary_pattern and not content_contains(content, binary_pattern):
|
|
228
263
|
continue
|
|
@@ -245,7 +280,8 @@ def search_content(config, matches: dict, pattern, binary_pattern,
|
|
|
245
280
|
ContentResult(
|
|
246
281
|
path=path_str,
|
|
247
282
|
virtual_path=virtual_path
|
|
248
|
-
)
|
|
283
|
+
),
|
|
284
|
+
result_lock
|
|
249
285
|
)
|
|
250
286
|
break
|
|
251
287
|
|
|
@@ -266,7 +302,8 @@ def search_content(config, matches: dict, pattern, binary_pattern,
|
|
|
266
302
|
path=path_str,
|
|
267
303
|
virtual_path=virtual_path,
|
|
268
304
|
groups=groups
|
|
269
|
-
)
|
|
305
|
+
),
|
|
306
|
+
result_lock
|
|
270
307
|
)
|
|
271
308
|
|
|
272
309
|
# Skip next block to avoid searching the contents of archive files
|
|
@@ -297,7 +334,8 @@ def search_content(config, matches: dict, pattern, binary_pattern,
|
|
|
297
334
|
matches,
|
|
298
335
|
result_queue,
|
|
299
336
|
'content',
|
|
300
|
-
ContentResult(path=path_str)
|
|
337
|
+
ContentResult(path=path_str),
|
|
338
|
+
result_lock
|
|
301
339
|
)
|
|
302
340
|
return
|
|
303
341
|
|
|
@@ -317,7 +355,8 @@ def search_content(config, matches: dict, pattern, binary_pattern,
|
|
|
317
355
|
ContentResult(
|
|
318
356
|
path=path_str,
|
|
319
357
|
groups=groups
|
|
320
|
-
)
|
|
358
|
+
),
|
|
359
|
+
result_lock
|
|
321
360
|
)
|
|
322
361
|
|
|
323
362
|
|
|
@@ -353,6 +392,10 @@ def walk_logic(path: Path, config, depth: int, matcher, ancestors: set):
|
|
|
353
392
|
# Prune explicitly excluded paths before further processing
|
|
354
393
|
if entry_path in config.exclude:
|
|
355
394
|
continue
|
|
395
|
+
# Filter by regex include and exclude
|
|
396
|
+
if (config.re_include and not config.re_include.search(entry_path.name)) \
|
|
397
|
+
or (config.re_exclude and config.re_exclude.search(entry_path.name) is not None):
|
|
398
|
+
continue
|
|
356
399
|
|
|
357
400
|
try:
|
|
358
401
|
is_dir = entry.is_dir(follow_symlinks=config.follow)
|
|
@@ -409,6 +452,7 @@ def walk_logic(path: Path, config, depth: int, matcher, ancestors: set):
|
|
|
409
452
|
ancestors.remove(dir_key)
|
|
410
453
|
|
|
411
454
|
|
|
455
|
+
# TODO: Make traversal parallelize just like seek func
|
|
412
456
|
def walk(config):
|
|
413
457
|
"""Include paths define traversal roots, avoiding unnecessary traversal
|
|
414
458
|
of unrelated parts of the tree"""
|
|
@@ -440,62 +484,158 @@ def walk(config):
|
|
|
440
484
|
yield from walk_logic(root, config, depth, matcher, set())
|
|
441
485
|
|
|
442
486
|
|
|
443
|
-
def
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
# If expression is simple and is a single TermNode, we can use binary pattern
|
|
447
|
-
if config.content:
|
|
448
|
-
if isinstance(pattern, TermNode):
|
|
449
|
-
binary_pattern = pattern.get_binary_pattern()
|
|
450
|
-
else:
|
|
451
|
-
binary_pattern = None
|
|
487
|
+
def process_candidate(item, config, pattern, binary_pattern,
|
|
488
|
+
matches, metrics, result_queue, result_lock):
|
|
489
|
+
p, metric_only = item
|
|
452
490
|
|
|
453
|
-
|
|
454
|
-
|
|
491
|
+
# Dir metrics must be added before should_skip,
|
|
492
|
+
# as it can influence these metrics
|
|
493
|
+
if config.stats and p.is_dir():
|
|
494
|
+
add_metric(
|
|
495
|
+
metrics,
|
|
496
|
+
result_queue,
|
|
497
|
+
'directories_scanned',
|
|
498
|
+
str(p),
|
|
499
|
+
result_lock
|
|
500
|
+
)
|
|
455
501
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
# as it can influence these metrics
|
|
459
|
-
if config.stats and p.is_dir():
|
|
460
|
-
add_metric(metrics, result_queue, 'directories_scanned', str(p))
|
|
502
|
+
if metric_only:
|
|
503
|
+
return
|
|
461
504
|
|
|
462
|
-
|
|
463
|
-
|
|
505
|
+
try:
|
|
506
|
+
p_ext = get_path_suffix(p)
|
|
507
|
+
# Path.state follows symlinks, so we use lstate here
|
|
508
|
+
p_size = p.lstat().st_size
|
|
509
|
+
except OSError:
|
|
510
|
+
return
|
|
464
511
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
# Path.state follows symlinks, so we use lstate here
|
|
468
|
-
p_size = p.lstat().st_size
|
|
469
|
-
except OSError:
|
|
470
|
-
continue
|
|
512
|
+
if should_skip(config, p, p_ext, p_size):
|
|
513
|
+
return
|
|
471
514
|
|
|
472
|
-
|
|
473
|
-
|
|
515
|
+
if config.stats:
|
|
516
|
+
path_str = str(p)
|
|
474
517
|
|
|
475
|
-
if config.
|
|
476
|
-
|
|
518
|
+
if (config.file or config.content) and p.is_file():
|
|
519
|
+
add_metric(
|
|
520
|
+
metrics,
|
|
521
|
+
result_queue,
|
|
522
|
+
'files_scanned',
|
|
523
|
+
path_str,
|
|
524
|
+
result_lock
|
|
525
|
+
)
|
|
477
526
|
|
|
478
|
-
|
|
479
|
-
|
|
527
|
+
if config.archive and p_ext in ARCHIVE_EXTS:
|
|
528
|
+
add_metric(
|
|
529
|
+
metrics,
|
|
530
|
+
result_queue,
|
|
531
|
+
'archives_scanned',
|
|
532
|
+
(path_str,),
|
|
533
|
+
result_lock
|
|
534
|
+
)
|
|
480
535
|
|
|
481
|
-
|
|
482
|
-
|
|
536
|
+
# Search for files and directories if requested
|
|
537
|
+
if config.file or config.directory:
|
|
538
|
+
search_file_and_dir(
|
|
539
|
+
config,
|
|
540
|
+
matches,
|
|
541
|
+
pattern,
|
|
542
|
+
p,
|
|
543
|
+
p_ext,
|
|
544
|
+
metrics,
|
|
545
|
+
result_queue,
|
|
546
|
+
result_lock
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
# Search for content inside files if requested
|
|
550
|
+
# Avoid empty files
|
|
551
|
+
if config.content and not p.is_symlink() \
|
|
552
|
+
and p.is_file() and p_size != 0:
|
|
553
|
+
search_content(
|
|
554
|
+
config,
|
|
555
|
+
matches,
|
|
556
|
+
pattern,
|
|
557
|
+
binary_pattern,
|
|
558
|
+
p,
|
|
559
|
+
p_ext,
|
|
560
|
+
metrics,
|
|
561
|
+
result_queue,
|
|
562
|
+
result_lock
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def batched(iterable, size):
|
|
567
|
+
# Convert iterable to iterator to prevent
|
|
568
|
+
# islice from creating identical batches.
|
|
569
|
+
# Example of using iterable:
|
|
570
|
+
# items = [1, 2, 3, 4, 5, 6]
|
|
571
|
+
# list(islice(items, 3)) -> [1, 2, 3]
|
|
572
|
+
# list(islice(items, 3)) -> Again [1, 2, 3]!
|
|
573
|
+
iterator = iter(iterable)
|
|
574
|
+
|
|
575
|
+
# If no elements remain, the batch becomes [],
|
|
576
|
+
# which causes while loop to terminate
|
|
577
|
+
while batch := list(islice(iterator, size)):
|
|
578
|
+
yield batch
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def process_batch(items, *args):
|
|
582
|
+
for item in items:
|
|
583
|
+
process_candidate(item, *args)
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def seek(config, result_queue=None) -> dict:
|
|
587
|
+
"""Main search function"""
|
|
588
|
+
pattern = parse_query_expression(config)
|
|
589
|
+
# If expression is simple and is a single TermNode, we can use binary pattern
|
|
590
|
+
binary_pattern = None
|
|
591
|
+
if config.content and isinstance(pattern, TermNode):
|
|
592
|
+
binary_pattern = pattern.get_binary_pattern()
|
|
593
|
+
|
|
594
|
+
matches = {
|
|
595
|
+
'file': [],
|
|
596
|
+
'directory': [],
|
|
597
|
+
'content': []
|
|
598
|
+
}
|
|
599
|
+
metrics = defaultdict(set)
|
|
600
|
+
result_lock = RLock()
|
|
601
|
+
|
|
602
|
+
workers = config.threads or (os.cpu_count() or 1)
|
|
603
|
+
|
|
604
|
+
with ThreadPoolExecutor(
|
|
605
|
+
max_workers=workers,
|
|
606
|
+
thread_name_prefix='pseek'
|
|
607
|
+
) as executor:
|
|
608
|
+
pending = set()
|
|
609
|
+
|
|
610
|
+
for batch in batched(walk(config), 16):
|
|
611
|
+
pending.add(
|
|
612
|
+
executor.submit(
|
|
613
|
+
process_batch,
|
|
614
|
+
batch,
|
|
615
|
+
config,
|
|
616
|
+
pattern,
|
|
617
|
+
binary_pattern,
|
|
618
|
+
matches,
|
|
483
619
|
metrics,
|
|
484
620
|
result_queue,
|
|
485
|
-
|
|
486
|
-
(path_str,)
|
|
621
|
+
result_lock
|
|
487
622
|
)
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
# Limit queued work to avoid excessive memory usage
|
|
626
|
+
# Using workers as max number of pending items
|
|
627
|
+
if len(pending) >= workers:
|
|
628
|
+
done, pending = wait(
|
|
629
|
+
pending,
|
|
630
|
+
return_when=FIRST_COMPLETED
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
# Don't ignore exceptions. If an exception occurs, re-raise it in main thread
|
|
634
|
+
for future in done:
|
|
635
|
+
future.result()
|
|
488
636
|
|
|
489
|
-
#
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
p_ext, metrics, result_queue)
|
|
493
|
-
|
|
494
|
-
# Search for content inside files if requested
|
|
495
|
-
# Avoid empty files
|
|
496
|
-
if config.content and not p.is_symlink() \
|
|
497
|
-
and p.is_file() and p_size != 0:
|
|
498
|
-
search_content(config, matches, pattern, binary_pattern, p,
|
|
499
|
-
p_ext, metrics, result_queue)
|
|
637
|
+
# Wait for workers that haven't finished their work yet
|
|
638
|
+
for future in pending:
|
|
639
|
+
future.result()
|
|
500
640
|
|
|
501
641
|
return matches, metrics
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
import re
|
|
3
|
-
from pathlib import Path
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
4
3
|
from dataclasses import dataclass, field
|
|
5
4
|
from math import isfinite
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from click import BadParameter
|
|
8
|
+
|
|
6
9
|
from .utils import compile_regex
|
|
7
10
|
|
|
8
11
|
SUFFIXES = {
|
|
@@ -166,6 +169,7 @@ class SearchConfig:
|
|
|
166
169
|
paths_only: bool
|
|
167
170
|
stats: bool
|
|
168
171
|
context: tuple[int, int]
|
|
172
|
+
threads: int
|
|
169
173
|
|
|
170
174
|
def __post_init__(self):
|
|
171
175
|
"""Post-initialization processing to normalize and validate inputs"""
|
|
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
|
|
File without changes
|