pseek 3.4.2__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.4.2 → pseek-3.6.0}/PKG-INFO +69 -1
- {pseek-3.4.2 → pseek-3.6.0}/README.md +68 -0
- {pseek-3.4.2 → pseek-3.6.0}/pseek/archive.py +11 -2
- {pseek-3.4.2 → pseek-3.6.0}/pseek/cli.py +23 -9
- {pseek-3.4.2 → pseek-3.6.0}/pseek/parser.py +6 -2
- {pseek-3.4.2 → pseek-3.6.0}/pseek/searcher.py +234 -90
- {pseek-3.4.2 → pseek-3.6.0}/pseek/structs.py +8 -3
- {pseek-3.4.2 → pseek-3.6.0}/pseek/utils.py +3 -1
- {pseek-3.4.2 → pseek-3.6.0}/pyproject.toml +1 -1
- {pseek-3.4.2 → pseek-3.6.0}/.gitattributes +0 -0
- {pseek-3.4.2 → pseek-3.6.0}/.github/workflows/publish.yml +0 -0
- {pseek-3.4.2 → pseek-3.6.0}/.gitignore +0 -0
- {pseek-3.4.2 → pseek-3.6.0}/Cargo.lock +0 -0
- {pseek-3.4.2 → pseek-3.6.0}/Cargo.toml +0 -0
- {pseek-3.4.2 → pseek-3.6.0}/LICENSE +0 -0
- {pseek-3.4.2 → pseek-3.6.0}/pseek/__init__.py +0 -0
- {pseek-3.4.2 → pseek-3.6.0}/pseek/compound_extensions +0 -0
- {pseek-3.4.2 → pseek-3.6.0}/scripts/check_version.py +0 -0
- {pseek-3.4.2 → 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
|
|
@@ -50,6 +50,7 @@ Fast and powerful command-line search tool for finding files, directories, and t
|
|
|
50
50
|
* [Regular Expression (Regex)](#regular-expression-search)
|
|
51
51
|
* [Fuzzy](#fuzzy-search)
|
|
52
52
|
* [Fuzzy Similarity Threshold](#fuzzy-similarity-threshold)
|
|
53
|
+
* [Full Path](#full-path)
|
|
53
54
|
* [Expression Queries](#expression-queries)
|
|
54
55
|
* [Operators](#supported-operators)
|
|
55
56
|
* [AND](#and)
|
|
@@ -74,6 +75,7 @@ Fast and powerful command-line search tool for finding files, directories, and t
|
|
|
74
75
|
* [Exclude](#exclude)
|
|
75
76
|
* [Size](#size-filtering)
|
|
76
77
|
* [Depth](#depth)
|
|
78
|
+
* [Parallel Search](#parallel-search)
|
|
77
79
|
* [Archive Search](#archive-search)
|
|
78
80
|
* [Nested Archives](#nested-archives)
|
|
79
81
|
* [Archive Filters](#archive-filters)
|
|
@@ -209,6 +211,7 @@ psk --exclude build
|
|
|
209
211
|
| `--word` | Match the whole word only (except when `--expr` is enabled, in which case you can make it match whole word by putting `w` before term: `w"foo"`) |
|
|
210
212
|
| `--expr` | Enable boolean query expressions. Example: `r"foo.*bar" and ("bar" or "baz") and not "qux"`. Prefixes: `r=regex`, `c=case-sensitive`, `w=whole-word`, `f=fuzzy` |
|
|
211
213
|
| `--context` | Show N context lines before and after matches, or use N:M to specify before:after |
|
|
214
|
+
| `--full-path` | By default, query is only matched against the filename or directory name (this applies only when searching for files or directories, not file contents). Using this flag, query is matched against the full path (and, if `--absolute-path` flag is enabled, against the absolute path) |
|
|
212
215
|
| `--depth` | Limit directory traversal to given depth range. By default, there is no limit on search depth |
|
|
213
216
|
| `--timeout` | Stop the search after the specified number of seconds |
|
|
214
217
|
| `--fuzzy` | Enable fuzzy search (Highlighting and counting matches are disabled in this mode if `--word` is not enabled to prevent the program from slowing down). except when `--expr` is enabled, in which case you can make it fuzzy by putting `f` before term: `f"foo"` |
|
|
@@ -234,6 +237,7 @@ psk --exclude build
|
|
|
234
237
|
| `--absolute-path` | Display full path of files and directories |
|
|
235
238
|
| `--paths-only` | Only show matching file paths for content search |
|
|
236
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 |
|
|
237
241
|
|
|
238
242
|
## Specifying the Root Directory
|
|
239
243
|
|
|
@@ -351,6 +355,45 @@ Examples:
|
|
|
351
355
|
|
|
352
356
|
Default: `80`
|
|
353
357
|
|
|
358
|
+
### Full Path
|
|
359
|
+
|
|
360
|
+
By default, Pseek matches file and directory queries against the entry's name only. Use `--full-path` to match against its full path within the search scope instead. This option affects file and directory searches, not content searches.
|
|
361
|
+
|
|
362
|
+
For example, given the relative path `pseek/utils.py`, you can match part of the path rather than just the filename:
|
|
363
|
+
|
|
364
|
+
```bash
|
|
365
|
+
# Match against the full relative path
|
|
366
|
+
psk "seek/utils" --full-path
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
**Relative paths vs. absolute paths**
|
|
370
|
+
|
|
371
|
+
`--full-path` does not automatically include the absolute path. If the file's absolute path is:
|
|
372
|
+
|
|
373
|
+
```text
|
|
374
|
+
/home/python/cli/pseek/utils.py
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
a query such as `python/cli` will not match unless `--absolute-path` is also enabled:
|
|
378
|
+
|
|
379
|
+
```bash
|
|
380
|
+
psk "python/cli" --full-path --absolute-path
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
**Nested archives**
|
|
384
|
+
|
|
385
|
+
When searching nested archives, `--full-path` applies only to the path within the current archive level, not the entire nested archive chain.
|
|
386
|
+
|
|
387
|
+
For example:
|
|
388
|
+
|
|
389
|
+
```text
|
|
390
|
+
archive.zip::folder/arc.zip::fd/file.txt
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
With `--full-path`, you can match `fd/f` against the entry `fd/file.txt` inside the innermost archive. A query such as `zip::fd` does not match across archive boundaries. Without `--full-path`, matching is limited to the entry name, `file.txt`.
|
|
394
|
+
|
|
395
|
+
> **Note:** For ordinary queries without `--regex`, backslashes are treated literally, so Windows paths can be searched without additional escaping. When using `--expr`, quoted terms use JSON-style escaping; escape backslashes accordingly. For example, `"\\"` represents a single literal backslash, while `"\\n"` represents the literal sequence `\n`.
|
|
396
|
+
|
|
354
397
|
## Expression Queries
|
|
355
398
|
|
|
356
399
|
Expression mode enables logical search expressions.
|
|
@@ -584,6 +627,8 @@ psk TODO \
|
|
|
584
627
|
|
|
585
628
|
Only search inside those paths.
|
|
586
629
|
|
|
630
|
+
> **Note:** Paths manually included by the user take precedence over other filters; this means that even if a path conflicts with rules related to [--glob](#globs), [--exclude](#exclude-paths), etc., it will still be included in the results.
|
|
631
|
+
|
|
587
632
|
#### Exclude Paths
|
|
588
633
|
|
|
589
634
|
```bash
|
|
@@ -677,6 +722,29 @@ project/ ← base directory
|
|
|
677
722
|
|
|
678
723
|
If `--depth` is not specified, the search has no depth limit.
|
|
679
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
|
+
|
|
680
748
|
## Archive Search
|
|
681
749
|
|
|
682
750
|
Enable archive support:
|
|
@@ -22,6 +22,7 @@ Fast and powerful command-line search tool for finding files, directories, and t
|
|
|
22
22
|
* [Regular Expression (Regex)](#regular-expression-search)
|
|
23
23
|
* [Fuzzy](#fuzzy-search)
|
|
24
24
|
* [Fuzzy Similarity Threshold](#fuzzy-similarity-threshold)
|
|
25
|
+
* [Full Path](#full-path)
|
|
25
26
|
* [Expression Queries](#expression-queries)
|
|
26
27
|
* [Operators](#supported-operators)
|
|
27
28
|
* [AND](#and)
|
|
@@ -46,6 +47,7 @@ Fast and powerful command-line search tool for finding files, directories, and t
|
|
|
46
47
|
* [Exclude](#exclude)
|
|
47
48
|
* [Size](#size-filtering)
|
|
48
49
|
* [Depth](#depth)
|
|
50
|
+
* [Parallel Search](#parallel-search)
|
|
49
51
|
* [Archive Search](#archive-search)
|
|
50
52
|
* [Nested Archives](#nested-archives)
|
|
51
53
|
* [Archive Filters](#archive-filters)
|
|
@@ -181,6 +183,7 @@ psk --exclude build
|
|
|
181
183
|
| `--word` | Match the whole word only (except when `--expr` is enabled, in which case you can make it match whole word by putting `w` before term: `w"foo"`) |
|
|
182
184
|
| `--expr` | Enable boolean query expressions. Example: `r"foo.*bar" and ("bar" or "baz") and not "qux"`. Prefixes: `r=regex`, `c=case-sensitive`, `w=whole-word`, `f=fuzzy` |
|
|
183
185
|
| `--context` | Show N context lines before and after matches, or use N:M to specify before:after |
|
|
186
|
+
| `--full-path` | By default, query is only matched against the filename or directory name (this applies only when searching for files or directories, not file contents). Using this flag, query is matched against the full path (and, if `--absolute-path` flag is enabled, against the absolute path) |
|
|
184
187
|
| `--depth` | Limit directory traversal to given depth range. By default, there is no limit on search depth |
|
|
185
188
|
| `--timeout` | Stop the search after the specified number of seconds |
|
|
186
189
|
| `--fuzzy` | Enable fuzzy search (Highlighting and counting matches are disabled in this mode if `--word` is not enabled to prevent the program from slowing down). except when `--expr` is enabled, in which case you can make it fuzzy by putting `f` before term: `f"foo"` |
|
|
@@ -206,6 +209,7 @@ psk --exclude build
|
|
|
206
209
|
| `--absolute-path` | Display full path of files and directories |
|
|
207
210
|
| `--paths-only` | Only show matching file paths for content search |
|
|
208
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 |
|
|
209
213
|
|
|
210
214
|
## Specifying the Root Directory
|
|
211
215
|
|
|
@@ -323,6 +327,45 @@ Examples:
|
|
|
323
327
|
|
|
324
328
|
Default: `80`
|
|
325
329
|
|
|
330
|
+
### Full Path
|
|
331
|
+
|
|
332
|
+
By default, Pseek matches file and directory queries against the entry's name only. Use `--full-path` to match against its full path within the search scope instead. This option affects file and directory searches, not content searches.
|
|
333
|
+
|
|
334
|
+
For example, given the relative path `pseek/utils.py`, you can match part of the path rather than just the filename:
|
|
335
|
+
|
|
336
|
+
```bash
|
|
337
|
+
# Match against the full relative path
|
|
338
|
+
psk "seek/utils" --full-path
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
**Relative paths vs. absolute paths**
|
|
342
|
+
|
|
343
|
+
`--full-path` does not automatically include the absolute path. If the file's absolute path is:
|
|
344
|
+
|
|
345
|
+
```text
|
|
346
|
+
/home/python/cli/pseek/utils.py
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
a query such as `python/cli` will not match unless `--absolute-path` is also enabled:
|
|
350
|
+
|
|
351
|
+
```bash
|
|
352
|
+
psk "python/cli" --full-path --absolute-path
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
**Nested archives**
|
|
356
|
+
|
|
357
|
+
When searching nested archives, `--full-path` applies only to the path within the current archive level, not the entire nested archive chain.
|
|
358
|
+
|
|
359
|
+
For example:
|
|
360
|
+
|
|
361
|
+
```text
|
|
362
|
+
archive.zip::folder/arc.zip::fd/file.txt
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
With `--full-path`, you can match `fd/f` against the entry `fd/file.txt` inside the innermost archive. A query such as `zip::fd` does not match across archive boundaries. Without `--full-path`, matching is limited to the entry name, `file.txt`.
|
|
366
|
+
|
|
367
|
+
> **Note:** For ordinary queries without `--regex`, backslashes are treated literally, so Windows paths can be searched without additional escaping. When using `--expr`, quoted terms use JSON-style escaping; escape backslashes accordingly. For example, `"\\"` represents a single literal backslash, while `"\\n"` represents the literal sequence `\n`.
|
|
368
|
+
|
|
326
369
|
## Expression Queries
|
|
327
370
|
|
|
328
371
|
Expression mode enables logical search expressions.
|
|
@@ -556,6 +599,8 @@ psk TODO \
|
|
|
556
599
|
|
|
557
600
|
Only search inside those paths.
|
|
558
601
|
|
|
602
|
+
> **Note:** Paths manually included by the user take precedence over other filters; this means that even if a path conflicts with rules related to [--glob](#globs), [--exclude](#exclude-paths), etc., it will still be included in the results.
|
|
603
|
+
|
|
559
604
|
#### Exclude Paths
|
|
560
605
|
|
|
561
606
|
```bash
|
|
@@ -649,6 +694,29 @@ project/ ← base directory
|
|
|
649
694
|
|
|
650
695
|
If `--depth` is not specified, the search has no depth limit.
|
|
651
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
|
+
|
|
652
720
|
## Archive Search
|
|
653
721
|
|
|
654
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
|
|
|
@@ -336,12 +342,14 @@ def check_rar_backend(archive_enabled: bool, tool_path: str, backend: str):
|
|
|
336
342
|
help='Fuzzy matching threshold (1-99). Higher values require closer matches.')
|
|
337
343
|
@click.option('-C', '--context', metavar='BEFORE:AFTER',
|
|
338
344
|
help='Show N context lines before and after matches, or use N:M to specify before:after.')
|
|
339
|
-
@click.option('--
|
|
345
|
+
@click.option('-p', '--full-path', is_flag=True,
|
|
346
|
+
help='By default, query is only matched against the filename or directory name '
|
|
347
|
+
'(this applies only when searching for files or directories, not file contents). '
|
|
348
|
+
'Using this flag, query is matched against the full path '
|
|
349
|
+
'(and, if --absolute-path flag is enabled, against the absolute path).')
|
|
350
|
+
@click.option('-D', '--depth', multiple=True, metavar='MIN:MAX',
|
|
340
351
|
help='Limit directory traversal to given depth range. '
|
|
341
352
|
'By default, there is no limit on search depth.')
|
|
342
|
-
@click.option('--timeout', metavar='FLOAT',
|
|
343
|
-
type=click.FloatRange(min=0, min_open=True),
|
|
344
|
-
help='Stop the search after the specified number of seconds.')
|
|
345
353
|
# Automatic filtering
|
|
346
354
|
@click.option('-u', '--unrestricted', is_flag=True,
|
|
347
355
|
help='Disable all automatic filters except the binary file filter for content search.')
|
|
@@ -419,6 +427,12 @@ def check_rar_backend(archive_enabled: bool, tool_path: str, backend: str):
|
|
|
419
427
|
@click.option('-l', '--paths-only', is_flag=True, help='Only show matching file paths for content search.')
|
|
420
428
|
@click.option('-S', '--stats', is_flag=True,
|
|
421
429
|
help='Show search statistics including result counts and search time.')
|
|
430
|
+
@click.option('--timeout', metavar='FLOAT',
|
|
431
|
+
type=click.FloatRange(min=0, min_open=True),
|
|
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.')
|
|
422
436
|
def search(**kwargs):
|
|
423
437
|
"""Search for files, directories, and file content based on the query."""
|
|
424
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)
|
|
@@ -60,14 +68,15 @@ def search_file_and_dir(config, matches: dict, pattern, p: Path,
|
|
|
60
68
|
'directory' if config.directory and p.is_dir() else
|
|
61
69
|
None
|
|
62
70
|
)
|
|
71
|
+
p_name = p.name if not config.full_path else path_str
|
|
63
72
|
|
|
64
|
-
if match_type and (pattern is None or pattern.evaluate(
|
|
73
|
+
if match_type and (pattern is None or pattern.evaluate(p_name)):
|
|
65
74
|
# Find matched query in the name
|
|
66
75
|
name_matches = find_matches(
|
|
67
76
|
pattern,
|
|
68
|
-
|
|
77
|
+
p_name,
|
|
69
78
|
# Calculate number of chars that come before name of file or dir
|
|
70
|
-
len(path_str) - len(
|
|
79
|
+
len(path_str) - len(p_name)
|
|
71
80
|
)
|
|
72
81
|
|
|
73
82
|
add_result(
|
|
@@ -77,37 +86,59 @@ def search_file_and_dir(config, matches: dict, pattern, p: Path,
|
|
|
77
86
|
FileDirResult(
|
|
78
87
|
path=path_str,
|
|
79
88
|
matches=name_matches
|
|
80
|
-
)
|
|
89
|
+
),
|
|
90
|
+
result_lock
|
|
81
91
|
)
|
|
82
92
|
|
|
83
93
|
# Search for files and directories name inside archive files if archive is active
|
|
84
94
|
if config.archive and p_ext in ARCHIVE_EXTS[:-3]:
|
|
85
95
|
for virtual_path, name, is_dir in extract_names_from_archive(p, config):
|
|
96
|
+
name_str = str(name)
|
|
97
|
+
|
|
86
98
|
if config.stats:
|
|
87
|
-
full_virtual_path = (path_str, *virtual_path,
|
|
99
|
+
full_virtual_path = (path_str, *virtual_path, name_str)
|
|
88
100
|
|
|
89
101
|
if not is_dir and config.file:
|
|
90
|
-
add_metric(
|
|
102
|
+
add_metric(
|
|
103
|
+
metrics,
|
|
104
|
+
result_queue,
|
|
105
|
+
'files_scanned',
|
|
106
|
+
full_virtual_path,
|
|
107
|
+
result_lock
|
|
108
|
+
)
|
|
91
109
|
elif is_dir:
|
|
92
|
-
add_metric(
|
|
110
|
+
add_metric(
|
|
111
|
+
metrics,
|
|
112
|
+
result_queue,
|
|
113
|
+
'directories_scanned',
|
|
114
|
+
full_virtual_path,
|
|
115
|
+
result_lock
|
|
116
|
+
)
|
|
93
117
|
|
|
94
118
|
# We cannot use "full_virtual_path[-1]" because it may be limited by --arc-depth
|
|
95
119
|
# and may not enter the archive at all to scan inside it
|
|
96
120
|
if len(full_virtual_path) >= 3 and get_path_suffix(full_virtual_path[-2]) in ARCHIVE_EXTS[:-3]:
|
|
97
|
-
add_metric(
|
|
121
|
+
add_metric(
|
|
122
|
+
metrics,
|
|
123
|
+
result_queue,
|
|
124
|
+
'archives_scanned',
|
|
125
|
+
full_virtual_path,
|
|
126
|
+
result_lock
|
|
127
|
+
)
|
|
98
128
|
|
|
99
129
|
arc_match_type = (
|
|
100
130
|
'file' if config.file and not is_dir else
|
|
101
131
|
'directory' if config.directory and is_dir else
|
|
102
132
|
None
|
|
103
133
|
)
|
|
134
|
+
virtual_name = name.name if not config.full_path else name_str
|
|
104
135
|
|
|
105
|
-
if arc_match_type and (pattern is None or pattern.evaluate(
|
|
136
|
+
if arc_match_type and (pattern is None or pattern.evaluate(virtual_name)):
|
|
106
137
|
name_matches = find_matches(
|
|
107
138
|
pattern,
|
|
108
|
-
|
|
139
|
+
virtual_name,
|
|
109
140
|
# Calculate number of chars that come before name of file or dir
|
|
110
|
-
len(
|
|
141
|
+
len(name_str) - len(virtual_name)
|
|
111
142
|
)
|
|
112
143
|
|
|
113
144
|
add_result(
|
|
@@ -117,8 +148,9 @@ def search_file_and_dir(config, matches: dict, pattern, p: Path,
|
|
|
117
148
|
FileDirResult(
|
|
118
149
|
path=path_str,
|
|
119
150
|
matches=name_matches,
|
|
120
|
-
virtual_path=[*virtual_path,
|
|
121
|
-
)
|
|
151
|
+
virtual_path=[*virtual_path, name_str]
|
|
152
|
+
),
|
|
153
|
+
result_lock
|
|
122
154
|
)
|
|
123
155
|
|
|
124
156
|
|
|
@@ -198,7 +230,7 @@ class ContextCollector:
|
|
|
198
230
|
|
|
199
231
|
|
|
200
232
|
def search_content(config, matches: dict, pattern, binary_pattern,
|
|
201
|
-
p: Path, p_ext: str, metrics, result_queue):
|
|
233
|
+
p: Path, p_ext: str, metrics, result_queue, result_lock):
|
|
202
234
|
"""Search within the contents of system files and files inside archive files"""
|
|
203
235
|
|
|
204
236
|
path_str = str(p)
|
|
@@ -213,12 +245,19 @@ def search_content(config, matches: dict, pattern, binary_pattern,
|
|
|
213
245
|
metrics,
|
|
214
246
|
result_queue,
|
|
215
247
|
'files_scanned',
|
|
216
|
-
full_virtual_path if virtual_path else path_str
|
|
248
|
+
full_virtual_path if virtual_path else path_str,
|
|
249
|
+
result_lock
|
|
217
250
|
)
|
|
218
251
|
|
|
219
252
|
if (len(full_virtual_path) >= 3 and get_path_suffix(full_virtual_path[-2]) in ARCHIVE_EXTS[:-3] \
|
|
220
253
|
or get_path_suffix(full_virtual_path[-1]) in ARCHIVE_EXTS[-3:]):
|
|
221
|
-
add_metric(
|
|
254
|
+
add_metric(
|
|
255
|
+
metrics,
|
|
256
|
+
result_queue,
|
|
257
|
+
'archives_scanned',
|
|
258
|
+
full_virtual_path,
|
|
259
|
+
result_lock
|
|
260
|
+
)
|
|
222
261
|
|
|
223
262
|
if binary_pattern and not content_contains(content, binary_pattern):
|
|
224
263
|
continue
|
|
@@ -241,7 +280,8 @@ def search_content(config, matches: dict, pattern, binary_pattern,
|
|
|
241
280
|
ContentResult(
|
|
242
281
|
path=path_str,
|
|
243
282
|
virtual_path=virtual_path
|
|
244
|
-
)
|
|
283
|
+
),
|
|
284
|
+
result_lock
|
|
245
285
|
)
|
|
246
286
|
break
|
|
247
287
|
|
|
@@ -262,7 +302,8 @@ def search_content(config, matches: dict, pattern, binary_pattern,
|
|
|
262
302
|
path=path_str,
|
|
263
303
|
virtual_path=virtual_path,
|
|
264
304
|
groups=groups
|
|
265
|
-
)
|
|
305
|
+
),
|
|
306
|
+
result_lock
|
|
266
307
|
)
|
|
267
308
|
|
|
268
309
|
# Skip next block to avoid searching the contents of archive files
|
|
@@ -293,7 +334,8 @@ def search_content(config, matches: dict, pattern, binary_pattern,
|
|
|
293
334
|
matches,
|
|
294
335
|
result_queue,
|
|
295
336
|
'content',
|
|
296
|
-
ContentResult(path=path_str)
|
|
337
|
+
ContentResult(path=path_str),
|
|
338
|
+
result_lock
|
|
297
339
|
)
|
|
298
340
|
return
|
|
299
341
|
|
|
@@ -313,7 +355,8 @@ def search_content(config, matches: dict, pattern, binary_pattern,
|
|
|
313
355
|
ContentResult(
|
|
314
356
|
path=path_str,
|
|
315
357
|
groups=groups
|
|
316
|
-
)
|
|
358
|
+
),
|
|
359
|
+
result_lock
|
|
317
360
|
)
|
|
318
361
|
|
|
319
362
|
|
|
@@ -349,6 +392,10 @@ def walk_logic(path: Path, config, depth: int, matcher, ancestors: set):
|
|
|
349
392
|
# Prune explicitly excluded paths before further processing
|
|
350
393
|
if entry_path in config.exclude:
|
|
351
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
|
|
352
399
|
|
|
353
400
|
try:
|
|
354
401
|
is_dir = entry.is_dir(follow_symlinks=config.follow)
|
|
@@ -405,6 +452,7 @@ def walk_logic(path: Path, config, depth: int, matcher, ancestors: set):
|
|
|
405
452
|
ancestors.remove(dir_key)
|
|
406
453
|
|
|
407
454
|
|
|
455
|
+
# TODO: Make traversal parallelize just like seek func
|
|
408
456
|
def walk(config):
|
|
409
457
|
"""Include paths define traversal roots, avoiding unnecessary traversal
|
|
410
458
|
of unrelated parts of the tree"""
|
|
@@ -436,62 +484,158 @@ def walk(config):
|
|
|
436
484
|
yield from walk_logic(root, config, depth, matcher, set())
|
|
437
485
|
|
|
438
486
|
|
|
439
|
-
def
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
# If expression is simple and is a single TermNode, we can use binary pattern
|
|
443
|
-
if config.content:
|
|
444
|
-
if isinstance(pattern, TermNode):
|
|
445
|
-
binary_pattern = pattern.get_binary_pattern()
|
|
446
|
-
else:
|
|
447
|
-
binary_pattern = None
|
|
487
|
+
def process_candidate(item, config, pattern, binary_pattern,
|
|
488
|
+
matches, metrics, result_queue, result_lock):
|
|
489
|
+
p, metric_only = item
|
|
448
490
|
|
|
449
|
-
|
|
450
|
-
|
|
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
|
+
)
|
|
451
501
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
# as it can influence these metrics
|
|
455
|
-
if config.stats and p.is_dir():
|
|
456
|
-
add_metric(metrics, result_queue, 'directories_scanned', str(p))
|
|
502
|
+
if metric_only:
|
|
503
|
+
return
|
|
457
504
|
|
|
458
|
-
|
|
459
|
-
|
|
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
|
|
460
511
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
# Path.state follows symlinks, so we use lstate here
|
|
464
|
-
p_size = p.lstat().st_size
|
|
465
|
-
except OSError:
|
|
466
|
-
continue
|
|
512
|
+
if should_skip(config, p, p_ext, p_size):
|
|
513
|
+
return
|
|
467
514
|
|
|
468
|
-
|
|
469
|
-
|
|
515
|
+
if config.stats:
|
|
516
|
+
path_str = str(p)
|
|
470
517
|
|
|
471
|
-
if config.
|
|
472
|
-
|
|
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
|
+
)
|
|
473
526
|
|
|
474
|
-
|
|
475
|
-
|
|
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
|
+
)
|
|
476
535
|
|
|
477
|
-
|
|
478
|
-
|
|
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,
|
|
479
619
|
metrics,
|
|
480
620
|
result_queue,
|
|
481
|
-
|
|
482
|
-
(path_str,)
|
|
621
|
+
result_lock
|
|
483
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()
|
|
484
636
|
|
|
485
|
-
#
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
p_ext, metrics, result_queue)
|
|
489
|
-
|
|
490
|
-
# Search for content inside files if requested
|
|
491
|
-
# Avoid empty files
|
|
492
|
-
if config.content and not p.is_symlink() \
|
|
493
|
-
and p.is_file() and p_size != 0:
|
|
494
|
-
search_content(config, matches, pattern, binary_pattern, p,
|
|
495
|
-
p_ext, metrics, result_queue)
|
|
637
|
+
# Wait for workers that haven't finished their work yet
|
|
638
|
+
for future in pending:
|
|
639
|
+
future.result()
|
|
496
640
|
|
|
497
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 = {
|
|
@@ -162,9 +165,11 @@ class SearchConfig:
|
|
|
162
165
|
arc_size: list[tuple]
|
|
163
166
|
rar_backend: str | None
|
|
164
167
|
absolute_path: bool
|
|
168
|
+
full_path: bool
|
|
165
169
|
paths_only: bool
|
|
166
170
|
stats: bool
|
|
167
171
|
context: tuple[int, int]
|
|
172
|
+
threads: int
|
|
168
173
|
|
|
169
174
|
def __post_init__(self):
|
|
170
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
|