pseek 2.4.0__tar.gz → 2.5.1__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.4.0 → pseek-2.5.1}/PKG-INFO +29 -24
- {pseek-2.4.0 → pseek-2.5.1}/README.md +27 -23
- {pseek-2.4.0 → pseek-2.5.1}/pseek/cli.py +30 -6
- {pseek-2.4.0 → pseek-2.5.1}/pseek/parser.py +83 -24
- {pseek-2.4.0 → pseek-2.5.1}/pseek/searcher.py +11 -8
- {pseek-2.4.0 → pseek-2.5.1}/pseek.egg-info/PKG-INFO +29 -24
- {pseek-2.4.0 → pseek-2.5.1}/pseek.egg-info/requires.txt +1 -0
- {pseek-2.4.0 → pseek-2.5.1}/pyproject.toml +3 -2
- {pseek-2.4.0 → pseek-2.5.1}/LICENSE +0 -0
- {pseek-2.4.0 → pseek-2.5.1}/pseek/__init__.py +0 -0
- {pseek-2.4.0 → pseek-2.5.1}/pseek/utils.py +0 -0
- {pseek-2.4.0 → pseek-2.5.1}/pseek.egg-info/SOURCES.txt +0 -0
- {pseek-2.4.0 → pseek-2.5.1}/pseek.egg-info/dependency_links.txt +0 -0
- {pseek-2.4.0 → pseek-2.5.1}/pseek.egg-info/entry_points.txt +0 -0
- {pseek-2.4.0 → pseek-2.5.1}/pseek.egg-info/top_level.txt +0 -0
- {pseek-2.4.0 → pseek-2.5.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pseek
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.5.1
|
|
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
|
|
@@ -12,6 +12,7 @@ Requires-Dist: click==8.1.8
|
|
|
12
12
|
Requires-Dist: lark==1.2.2
|
|
13
13
|
Requires-Dist: py7zr==1.0.0
|
|
14
14
|
Requires-Dist: rarfile==4.2
|
|
15
|
+
Requires-Dist: rapidfuzz==3.13.0
|
|
15
16
|
Dynamic: license-file
|
|
16
17
|
|
|
17
18
|
# Pseek
|
|
@@ -43,7 +44,9 @@ pip install pseek
|
|
|
43
44
|
```sh
|
|
44
45
|
git clone https://github.com/ArianN8610/pysearch.git
|
|
45
46
|
cd pysearch
|
|
46
|
-
|
|
47
|
+
python -m venv venv
|
|
48
|
+
[Activate venv]
|
|
49
|
+
pip install click==8.1.8 lark==1.2.2 py7zr==1.0.0 rarfile==4.2 rapidfuzz==3.13.0
|
|
47
50
|
```
|
|
48
51
|
|
|
49
52
|
## Usage
|
|
@@ -81,28 +84,30 @@ pseek "error\d+" --regex
|
|
|
81
84
|
|
|
82
85
|
## Command Options
|
|
83
86
|
|
|
84
|
-
| Option | Description
|
|
85
|
-
|
|
86
|
-
| `--path` | Base directory to search in (default: current directory `.`)
|
|
87
|
-
| `--file` | Search only in file names
|
|
88
|
-
| `--directory` | Search only in directory names
|
|
89
|
-
| `--content` | Search inside file contents
|
|
90
|
-
| `--ext`, `--exclude-ext` | Filter by file extension (e.g., `txt`, `log`)
|
|
91
|
-
| `--case-sensitive` | Make the search case-sensitive (except when `--expr` is enabled, in which case you can make it case sensitive by putting `c` before term: `c"foo"`)
|
|
92
|
-
| `--regex` | Use regular expressions to search (except when `--expr` is enabled, in which case you can make it regex by putting `r` before term: `r"foo"`)
|
|
93
|
-
| `--include`, `--exclude` | Limit search results to specific set of directories or files
|
|
94
|
-
| `--re-include`, `--re-exclude` | Limit search results to specific directories or files with regex
|
|
95
|
-
| `--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"`)
|
|
96
|
-
| `--expr` | Enable to write conditions in the query. Example: `r"foo.*bar" and ("bar" or "baz") and not "qux"` (To use regex, word,
|
|
97
|
-
| `--timeout` | To stop the search after a specified period of time (Seconds)
|
|
98
|
-
| `--
|
|
99
|
-
| `--
|
|
100
|
-
| `--
|
|
101
|
-
| `--
|
|
102
|
-
| `--arc-
|
|
103
|
-
| `--
|
|
104
|
-
| `--
|
|
105
|
-
| `--
|
|
87
|
+
| Option | Description |
|
|
88
|
+
|--------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
89
|
+
| `--path` | Base directory to search in (default: current directory `.`) |
|
|
90
|
+
| `--file` | Search only in file names |
|
|
91
|
+
| `--directory` | Search only in directory names |
|
|
92
|
+
| `--content` | Search inside file contents |
|
|
93
|
+
| `--ext`, `--exclude-ext` | Filter by file extension (e.g., `txt`, `log`) |
|
|
94
|
+
| `--case-sensitive` | Make the search case-sensitive (except when `--expr` is enabled, in which case you can make it case sensitive by putting `c` before term: `c"foo"`) |
|
|
95
|
+
| `--regex` | Use regular expressions to search (except when `--expr` is enabled, in which case you can make it regex by putting `r` before term: `r"foo"`) |
|
|
96
|
+
| `--include`, `--exclude` | Limit search results to specific set of directories or files |
|
|
97
|
+
| `--re-include`, `--re-exclude` | Limit search results to specific directories or files with regex |
|
|
98
|
+
| `--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"`) |
|
|
99
|
+
| `--expr` | Enable to write conditions in the query. Example: `r"foo.*bar" and ("bar" or "baz") and not "qux"` (To use regex, word, case-sensitive, and fuzzy features, you can use the prefixes `r`, `w`, `c`, and `f` before terms. Allowed modes: `r`, `c`, `w`, `f`, `rc`, `cr`, `cw`, `wc`, `cf`, `fc`, `wf`, `fw`, `cwf`, `cfw`, `wcf`, `wfc`, `fcw`, `fwc`. Examples: `r"foo.*bar"`, `wcf"Aple"`, `cr".*Foo"`, ...) |
|
|
100
|
+
| `--timeout` | To stop the search after a specified period of time (Seconds) |
|
|
101
|
+
| `--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"` |
|
|
102
|
+
| `--fuzzy_level` | Similarity threshold from 0 to 99 for fuzzy search (default: `80`) |
|
|
103
|
+
| `--max-size`, `--min-size` | Specify maximum and minimum sizes for files and directories |
|
|
104
|
+
| `--archive` | Enable search within archive files (e.g. `zip`, `rar`, `7z`, `gz`, `bz2`, `xz`, `tar`, `tar.gz`, `tar.bz2`, `tar.xz`) |
|
|
105
|
+
| `--arc-ext`, `--arc-ee` | Filter by file extension inside archive files |
|
|
106
|
+
| `--arc-inc`, `--arc-exc` | Limit search results to specific set of directories or files inside archive files |
|
|
107
|
+
| `--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) |
|
|
108
|
+
| `--rarfb` | Path to RAR backend tool (e.g. UnRAR.exe, ...) |
|
|
109
|
+
| `--full-path` | Display full path of files and directories |
|
|
110
|
+
| `--no-content` | Only display files path for content search |
|
|
106
111
|
|
|
107
112
|
## Requirements
|
|
108
113
|
|
|
@@ -27,7 +27,9 @@ pip install pseek
|
|
|
27
27
|
```sh
|
|
28
28
|
git clone https://github.com/ArianN8610/pysearch.git
|
|
29
29
|
cd pysearch
|
|
30
|
-
|
|
30
|
+
python -m venv venv
|
|
31
|
+
[Activate venv]
|
|
32
|
+
pip install click==8.1.8 lark==1.2.2 py7zr==1.0.0 rarfile==4.2 rapidfuzz==3.13.0
|
|
31
33
|
```
|
|
32
34
|
|
|
33
35
|
## Usage
|
|
@@ -65,28 +67,30 @@ pseek "error\d+" --regex
|
|
|
65
67
|
|
|
66
68
|
## Command Options
|
|
67
69
|
|
|
68
|
-
| Option | Description
|
|
69
|
-
|
|
70
|
-
| `--path` | Base directory to search in (default: current directory `.`)
|
|
71
|
-
| `--file` | Search only in file names
|
|
72
|
-
| `--directory` | Search only in directory names
|
|
73
|
-
| `--content` | Search inside file contents
|
|
74
|
-
| `--ext`, `--exclude-ext` | Filter by file extension (e.g., `txt`, `log`)
|
|
75
|
-
| `--case-sensitive` | Make the search case-sensitive (except when `--expr` is enabled, in which case you can make it case sensitive by putting `c` before term: `c"foo"`)
|
|
76
|
-
| `--regex` | Use regular expressions to search (except when `--expr` is enabled, in which case you can make it regex by putting `r` before term: `r"foo"`)
|
|
77
|
-
| `--include`, `--exclude` | Limit search results to specific set of directories or files
|
|
78
|
-
| `--re-include`, `--re-exclude` | Limit search results to specific directories or files with regex
|
|
79
|
-
| `--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"`)
|
|
80
|
-
| `--expr` | Enable to write conditions in the query. Example: `r"foo.*bar" and ("bar" or "baz") and not "qux"` (To use regex, word,
|
|
81
|
-
| `--timeout` | To stop the search after a specified period of time (Seconds)
|
|
82
|
-
| `--
|
|
83
|
-
| `--
|
|
84
|
-
| `--
|
|
85
|
-
| `--
|
|
86
|
-
| `--arc-
|
|
87
|
-
| `--
|
|
88
|
-
| `--
|
|
89
|
-
| `--
|
|
70
|
+
| Option | Description |
|
|
71
|
+
|--------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
72
|
+
| `--path` | Base directory to search in (default: current directory `.`) |
|
|
73
|
+
| `--file` | Search only in file names |
|
|
74
|
+
| `--directory` | Search only in directory names |
|
|
75
|
+
| `--content` | Search inside file contents |
|
|
76
|
+
| `--ext`, `--exclude-ext` | Filter by file extension (e.g., `txt`, `log`) |
|
|
77
|
+
| `--case-sensitive` | Make the search case-sensitive (except when `--expr` is enabled, in which case you can make it case sensitive by putting `c` before term: `c"foo"`) |
|
|
78
|
+
| `--regex` | Use regular expressions to search (except when `--expr` is enabled, in which case you can make it regex by putting `r` before term: `r"foo"`) |
|
|
79
|
+
| `--include`, `--exclude` | Limit search results to specific set of directories or files |
|
|
80
|
+
| `--re-include`, `--re-exclude` | Limit search results to specific directories or files with regex |
|
|
81
|
+
| `--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"`) |
|
|
82
|
+
| `--expr` | Enable to write conditions in the query. Example: `r"foo.*bar" and ("bar" or "baz") and not "qux"` (To use regex, word, case-sensitive, and fuzzy features, you can use the prefixes `r`, `w`, `c`, and `f` before terms. Allowed modes: `r`, `c`, `w`, `f`, `rc`, `cr`, `cw`, `wc`, `cf`, `fc`, `wf`, `fw`, `cwf`, `cfw`, `wcf`, `wfc`, `fcw`, `fwc`. Examples: `r"foo.*bar"`, `wcf"Aple"`, `cr".*Foo"`, ...) |
|
|
83
|
+
| `--timeout` | To stop the search after a specified period of time (Seconds) |
|
|
84
|
+
| `--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"` |
|
|
85
|
+
| `--fuzzy_level` | Similarity threshold from 0 to 99 for fuzzy search (default: `80`) |
|
|
86
|
+
| `--max-size`, `--min-size` | Specify maximum and minimum sizes for files and directories |
|
|
87
|
+
| `--archive` | Enable search within archive files (e.g. `zip`, `rar`, `7z`, `gz`, `bz2`, `xz`, `tar`, `tar.gz`, `tar.bz2`, `tar.xz`) |
|
|
88
|
+
| `--arc-ext`, `--arc-ee` | Filter by file extension inside archive files |
|
|
89
|
+
| `--arc-inc`, `--arc-exc` | Limit search results to specific set of directories or files inside archive files |
|
|
90
|
+
| `--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) |
|
|
91
|
+
| `--rarfb` | Path to RAR backend tool (e.g. UnRAR.exe, ...) |
|
|
92
|
+
| `--full-path` | Display full path of files and directories |
|
|
93
|
+
| `--no-content` | Only display files path for content search |
|
|
90
94
|
|
|
91
95
|
## Requirements
|
|
92
96
|
|
|
@@ -23,7 +23,6 @@ def run_search_process(file, directory, content, search_instance):
|
|
|
23
23
|
click.echo(click.style(message, fg='red'))
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
|
|
27
26
|
@click.command()
|
|
28
27
|
@click.argument('query')
|
|
29
28
|
@click.option('-p', '--path', type=click.Path(exists=True, file_okay=False, dir_okay=True),
|
|
@@ -47,11 +46,17 @@ def run_search_process(file, directory, content, search_instance):
|
|
|
47
46
|
'in which case you can make it match whole word by putting w before term: w"foo")')
|
|
48
47
|
@click.option('--expr', is_flag=True,
|
|
49
48
|
help='Enable to write conditions in the query. Example: r"foo.*bar" and ("bar" or "baz") and not "qux" '
|
|
50
|
-
'(To use regex, word,
|
|
51
|
-
'you can use the prefixes r, w, and
|
|
52
|
-
'
|
|
49
|
+
'(To use regex, word, case-sensitive, and fuzzy features, '
|
|
50
|
+
'you can use the prefixes r, w, c, and f before terms. Allowed modes: '
|
|
51
|
+
'r, c, w, f, rc, cr, cw, wc, cf, fc, wf, fw, cwf, cfw, wcf, wfc, fcw, fwc. '
|
|
52
|
+
'Examples: r"foo.*bar", wcf"Aple", cr".*Foo", ...)')
|
|
53
53
|
@click.option('--timeout', type=click.INT,
|
|
54
54
|
help='To stop the search after a specified period of time (Seconds)')
|
|
55
|
+
@click.option('--fuzzy', is_flag=True, help='Enable fuzzy search (approximate matching). '
|
|
56
|
+
'except when --expr is enabled, '
|
|
57
|
+
'in which case you can make it fuzzy by putting f before term: f"foo"')
|
|
58
|
+
@click.option('--fuzzy-level', type=click.IntRange(0, 99), default=80, show_default=True,
|
|
59
|
+
help='Similarity threshold from 0 to 99 for fuzzy search.')
|
|
55
60
|
# Extension filters
|
|
56
61
|
@click.option('--ext', multiple=True, type=click.STRING,
|
|
57
62
|
help='Include files with these extensions. Example: --ext py --ext js')
|
|
@@ -89,12 +94,29 @@ def run_search_process(file, directory, content, search_instance):
|
|
|
89
94
|
@click.option('--full-path', is_flag=True, help='Display full paths for results.')
|
|
90
95
|
@click.option('--no-content', is_flag=True, help='Only display files path for content search.')
|
|
91
96
|
def search(query, path, file, directory, content, case_sensitive, ext, exclude_ext, regex, include, exclude,
|
|
92
|
-
re_include, re_exclude, word, expr, timeout, max_size, min_size, archive, arc_ext,
|
|
93
|
-
arc_exc, arc_max, arc_min, rarfb, full_path, no_content):
|
|
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):
|
|
94
99
|
"""Search for files, directories, and file content based on the query."""
|
|
95
100
|
|
|
96
101
|
check_rar_backend(archive, rarfb, query)
|
|
97
102
|
|
|
103
|
+
if not expr and fuzzy:
|
|
104
|
+
if not word:
|
|
105
|
+
click.echo(
|
|
106
|
+
click.style(
|
|
107
|
+
"Warning: Fuzzy substring highlighting and counting matches are disabled to improve performance.\n",
|
|
108
|
+
fg="yellow"
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
elif word and " " in query:
|
|
112
|
+
click.echo(
|
|
113
|
+
click.style(
|
|
114
|
+
'Warning: When using "--fuzzy" and "--word", it is better to have the query be a word and '
|
|
115
|
+
'not a phrase, as this will cause errors in the results.\n',
|
|
116
|
+
fg="yellow"
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
|
|
98
120
|
# If no search type is specified, search in all types.
|
|
99
121
|
if not any((file, directory, content)):
|
|
100
122
|
file = directory = content = True
|
|
@@ -113,6 +135,8 @@ def search(query, path, file, directory, content, case_sensitive, ext, exclude_e
|
|
|
113
135
|
re_exclude=re_exclude,
|
|
114
136
|
whole_word=word,
|
|
115
137
|
expr=expr,
|
|
138
|
+
fuzzy=fuzzy,
|
|
139
|
+
fuzzy_level=fuzzy_level,
|
|
116
140
|
max_size=max_size,
|
|
117
141
|
min_size=min_size,
|
|
118
142
|
archive=archive,
|
|
@@ -3,6 +3,7 @@ import sys
|
|
|
3
3
|
import click
|
|
4
4
|
from lark import Lark, Transformer
|
|
5
5
|
from .utils import compile_regex
|
|
6
|
+
from rapidfuzz import fuzz
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
class ExprNode:
|
|
@@ -13,31 +14,64 @@ class ExprNode:
|
|
|
13
14
|
|
|
14
15
|
class TermNode(ExprNode):
|
|
15
16
|
"""Node representing a single search term"""
|
|
16
|
-
def __init__(self, term: str, regex, whole_word, case_sensitive):
|
|
17
|
+
def __init__(self, term: str, regex, whole_word, case_sensitive, fuzzy, fuzzy_level):
|
|
17
18
|
self.raw_term = term
|
|
18
19
|
self.whole_word = whole_word
|
|
19
20
|
self.case_sensitive = case_sensitive
|
|
21
|
+
self.fuzzy = fuzzy
|
|
22
|
+
self.fuzzy_level = fuzzy_level
|
|
20
23
|
|
|
21
|
-
|
|
24
|
+
if not fuzzy:
|
|
25
|
+
flags = 0 if case_sensitive else re.IGNORECASE # Adjust case sensitivity
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
# Build the regex pattern
|
|
28
|
+
if not regex:
|
|
29
|
+
term = re.escape(term) # Escape if not regex
|
|
30
|
+
# Apply whole-word matching only if not using regex (or if desired behavior is defined)
|
|
31
|
+
if whole_word:
|
|
32
|
+
term = r'\b' + term + r'\b'
|
|
29
33
|
|
|
30
|
-
|
|
34
|
+
self.pattern = compile_regex(term, flags) # Precompile the regex pattern for performance
|
|
31
35
|
|
|
32
36
|
def evaluate(self, text: str) -> bool:
|
|
33
|
-
|
|
37
|
+
if not self.fuzzy:
|
|
38
|
+
return bool(self.pattern.search(text))
|
|
39
|
+
|
|
40
|
+
# Fuzzy search mode
|
|
41
|
+
text_cmp = text if self.case_sensitive else text.lower()
|
|
42
|
+
term = self.raw_term if self.case_sensitive else self.raw_term.lower()
|
|
43
|
+
|
|
44
|
+
if self.whole_word:
|
|
45
|
+
words = re.findall(r'\w+', text_cmp)
|
|
46
|
+
return any(fuzz.ratio(term, word) >= self.fuzzy_level for word in words)
|
|
47
|
+
else:
|
|
48
|
+
# Use the correct method depending on the len of str to increase accuracy and avoid illogical matching
|
|
49
|
+
if len(text_cmp) > len(term):
|
|
50
|
+
score = fuzz.partial_ratio(term, text_cmp)
|
|
51
|
+
else:
|
|
52
|
+
score = fuzz.ratio(term, text_cmp)
|
|
53
|
+
return score >= self.fuzzy_level
|
|
54
|
+
|
|
34
55
|
|
|
35
56
|
def count_matches(self, text: str) -> int:
|
|
36
|
-
"""Count how many times the pattern appears in the text"""
|
|
37
|
-
|
|
57
|
+
"""Count how many times the pattern or fuzzy term appears in the text"""
|
|
58
|
+
if not self.fuzzy:
|
|
59
|
+
return len(list(self.pattern.finditer(text)))
|
|
60
|
+
|
|
61
|
+
text_cmp = text if self.case_sensitive else text.lower()
|
|
62
|
+
term = self.raw_term if self.case_sensitive else self.raw_term.lower()
|
|
63
|
+
|
|
64
|
+
if self.whole_word:
|
|
65
|
+
words = re.findall(r'\w+', text_cmp)
|
|
66
|
+
return sum(1 for word in words if fuzz.ratio(term, word) >= self.fuzzy_level)
|
|
67
|
+
|
|
68
|
+
# Optionally could implement a sliding window here, but it's expensive
|
|
69
|
+
return 0
|
|
38
70
|
|
|
39
71
|
def get_binary_pattern(self) -> re.Pattern:
|
|
40
72
|
"""Return a compiled binary regex pattern"""
|
|
73
|
+
if self.fuzzy:
|
|
74
|
+
raise NotImplementedError("Binary pattern is not supported for fuzzy matching.")
|
|
41
75
|
return re.compile(self.pattern.pattern.encode("utf-8"), self.pattern.flags)
|
|
42
76
|
|
|
43
77
|
|
|
@@ -89,7 +123,7 @@ query_grammar = r"""
|
|
|
89
123
|
| ESCAPED_STRING -> string
|
|
90
124
|
| "(" expr ")"
|
|
91
125
|
|
|
92
|
-
PREFIXED_STRING: /(r|c|w|rc|cr|cw|wc)"([^"\\]|\\.)*"/
|
|
126
|
+
PREFIXED_STRING: /(r|c|w|f|rc|cr|cw|wc|cf|fc|wf|fw|cwf|cfw|wcf|wfc|fcw|fwc)"([^"\\]|\\.)*"/
|
|
93
127
|
|
|
94
128
|
%import common.ESCAPED_STRING
|
|
95
129
|
%import common.WS
|
|
@@ -99,8 +133,9 @@ PREFIXED_STRING: /(r|c|w|rc|cr|cw|wc)"([^"\\]|\\.)*"/
|
|
|
99
133
|
|
|
100
134
|
class TreeToExpr(Transformer):
|
|
101
135
|
"""Transform parsed tree into expression tree (ExprNode subclasses)"""
|
|
102
|
-
def __init__(self):
|
|
136
|
+
def __init__(self, fuzzy_level):
|
|
103
137
|
super().__init__()
|
|
138
|
+
self.fuzzy_level = fuzzy_level
|
|
104
139
|
|
|
105
140
|
def string(self, s):
|
|
106
141
|
""" Match normal quoted string: "foo" """
|
|
@@ -109,7 +144,9 @@ class TreeToExpr(Transformer):
|
|
|
109
144
|
term,
|
|
110
145
|
False,
|
|
111
146
|
False,
|
|
112
|
-
False
|
|
147
|
+
False,
|
|
148
|
+
False,
|
|
149
|
+
None
|
|
113
150
|
)
|
|
114
151
|
|
|
115
152
|
def prefixed_string(self, s):
|
|
@@ -122,6 +159,8 @@ class TreeToExpr(Transformer):
|
|
|
122
159
|
regex='r' in prefix,
|
|
123
160
|
whole_word='w' in prefix,
|
|
124
161
|
case_sensitive='c' in prefix,
|
|
162
|
+
fuzzy='f' in prefix,
|
|
163
|
+
fuzzy_level=self.fuzzy_level
|
|
125
164
|
)
|
|
126
165
|
|
|
127
166
|
def and_expr(self, args):
|
|
@@ -134,33 +173,50 @@ class TreeToExpr(Transformer):
|
|
|
134
173
|
return NotNode(args[0])
|
|
135
174
|
|
|
136
175
|
|
|
137
|
-
def parse_query_expression(query: str, expr, regex, whole_word, case_sensitive) -> ExprNode:
|
|
176
|
+
def parse_query_expression(query: str, expr, regex, whole_word, case_sensitive, fuzzy, fuzzy_level) -> ExprNode:
|
|
138
177
|
"""
|
|
139
178
|
Function to parse the query and return expression tree.
|
|
140
179
|
If expr is False, treat the whole query as a single term.
|
|
141
180
|
"""
|
|
142
181
|
|
|
143
182
|
if not expr:
|
|
144
|
-
return TermNode(query, regex, whole_word, case_sensitive)
|
|
183
|
+
return TermNode(query, regex, whole_word, case_sensitive, fuzzy, fuzzy_level)
|
|
145
184
|
|
|
146
185
|
# Otherwise, parse using Lark
|
|
147
186
|
parser = Lark(query_grammar, parser="lalr")
|
|
148
187
|
try:
|
|
149
188
|
tree = parser.parse(query)
|
|
150
|
-
return TreeToExpr().transform(tree)
|
|
189
|
+
return TreeToExpr(fuzzy_level).transform(tree)
|
|
151
190
|
except Exception as e:
|
|
152
191
|
click.echo(click.style("Query parser error:\n\n", fg='red') + str(e))
|
|
153
192
|
sys.exit(1)
|
|
154
193
|
|
|
155
194
|
|
|
156
|
-
def
|
|
195
|
+
def highlight_text(expr: ExprNode, text: str, fuzzy: bool) -> str:
|
|
196
|
+
"""
|
|
197
|
+
Highlight matching parts of the text.
|
|
198
|
+
Only highlights fuzzy matches when whole_word=True.
|
|
199
|
+
"""
|
|
157
200
|
matches = []
|
|
158
201
|
|
|
159
202
|
def collect_matches(node):
|
|
160
203
|
if isinstance(node, TermNode):
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
204
|
+
# Skip fuzzy highlight if whole_word is False
|
|
205
|
+
if node.fuzzy:
|
|
206
|
+
if not node.whole_word:
|
|
207
|
+
return # skip highlighting
|
|
208
|
+
text_cmp = text if node.case_sensitive else text.lower()
|
|
209
|
+
term = node.raw_term if node.case_sensitive else node.raw_term.lower()
|
|
210
|
+
|
|
211
|
+
# collect word matches
|
|
212
|
+
for match in re.finditer(r'\w+', text_cmp):
|
|
213
|
+
word = match.group()
|
|
214
|
+
if fuzz.ratio(term, word) >= node.fuzzy_level:
|
|
215
|
+
matches.append((match.start(), match.end()))
|
|
216
|
+
else:
|
|
217
|
+
for match in node.pattern.finditer(text):
|
|
218
|
+
matches.append((match.start(), match.end()))
|
|
219
|
+
elif isinstance(node, AndNode) or isinstance(node, OrNode):
|
|
164
220
|
collect_matches(node.left)
|
|
165
221
|
collect_matches(node.right)
|
|
166
222
|
elif isinstance(node, NotNode):
|
|
@@ -168,14 +224,17 @@ def highlight_text_safe(expr: ExprNode, text: str) -> str:
|
|
|
168
224
|
|
|
169
225
|
collect_matches(expr)
|
|
170
226
|
|
|
171
|
-
#
|
|
227
|
+
# Sort and merge overlapping matches (for example, if one match was inside another match)
|
|
172
228
|
matches.sort() # sort by start position
|
|
173
229
|
merged = []
|
|
174
230
|
for start, end in matches:
|
|
175
231
|
if not merged or start >= merged[-1][1]: # no overlap
|
|
176
232
|
merged.append((start, end))
|
|
233
|
+
else:
|
|
234
|
+
# Merge overlapping
|
|
235
|
+
merged[-1] = (merged[-1][0], max(merged[-1][1], end))
|
|
177
236
|
|
|
178
|
-
#
|
|
237
|
+
# Build highlighted text
|
|
179
238
|
result = []
|
|
180
239
|
last = 0
|
|
181
240
|
for start, end in merged:
|
|
@@ -2,7 +2,7 @@ import mmap
|
|
|
2
2
|
import click
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from .utils import compile_regex, get_archive_path_size, try_decode
|
|
5
|
-
from .parser import parse_query_expression, TermNode,
|
|
5
|
+
from .parser import parse_query_expression, TermNode, highlight_text
|
|
6
6
|
from concurrent.futures import ThreadPoolExecutor
|
|
7
7
|
import zipfile
|
|
8
8
|
import py7zr
|
|
@@ -30,8 +30,8 @@ EXCLUDED_EXTENSIONS = (
|
|
|
30
30
|
|
|
31
31
|
class Search:
|
|
32
32
|
def __init__(self, base_path, query, case_sensitive, ext, exclude_ext, regex, include, exclude, re_include,
|
|
33
|
-
re_exclude, whole_word, expr, max_size, min_size, archive, arc_ext, arc_ee,
|
|
34
|
-
arc_max, arc_min, full_path, no_content):
|
|
33
|
+
re_exclude, whole_word, expr, fuzzy, fuzzy_level, max_size, min_size, archive, arc_ext, arc_ee,
|
|
34
|
+
arc_inc, arc_exc, arc_max, arc_min, full_path, no_content):
|
|
35
35
|
"""Initialize search parameters"""
|
|
36
36
|
self.base_path = Path(base_path)
|
|
37
37
|
self.query = query
|
|
@@ -45,6 +45,8 @@ class Search:
|
|
|
45
45
|
self.re_exclude = compile_regex(re_exclude)
|
|
46
46
|
self.whole_word = whole_word
|
|
47
47
|
self.expr = expr
|
|
48
|
+
self.fuzzy = fuzzy
|
|
49
|
+
self.fuzzy_level = fuzzy_level
|
|
48
50
|
self.max_size = max_size
|
|
49
51
|
self.min_size = min_size
|
|
50
52
|
self.archive = archive
|
|
@@ -301,7 +303,8 @@ class Search:
|
|
|
301
303
|
|
|
302
304
|
def search(self, search_type: str):
|
|
303
305
|
"""Main search function. search_type can be 'file', 'directory' or 'content'"""
|
|
304
|
-
pattern = parse_query_expression(self.query, self.expr, self.regex, self.whole_word, self.case_sensitive
|
|
306
|
+
pattern = parse_query_expression(self.query, self.expr, self.regex, self.whole_word, self.case_sensitive,
|
|
307
|
+
self.fuzzy, self.fuzzy_level)
|
|
305
308
|
|
|
306
309
|
if search_type in ('file', 'directory'):
|
|
307
310
|
matches = []
|
|
@@ -320,14 +323,14 @@ class Search:
|
|
|
320
323
|
|
|
321
324
|
if pattern.evaluate(p.name) and not (search_type == 'directory' and p_resolved.is_file()):
|
|
322
325
|
# Highlight matched query in the name
|
|
323
|
-
highlighted_name =
|
|
326
|
+
highlighted_name = highlight_text(pattern, p.name, self.fuzzy)
|
|
324
327
|
matches.append(f'{p_parent}\\{highlighted_name}')
|
|
325
328
|
|
|
326
329
|
# Search for files and directories name inside archive files if archive is active
|
|
327
330
|
if self.archive and p_ext in ('zip', 'rar', '7z', 'tar', 'tar.gz', 'tar.bz2', 'tar.xz'):
|
|
328
331
|
for name in self.extract_names_from_archive(p_resolved, search_type):
|
|
329
332
|
if pattern.evaluate(name.name):
|
|
330
|
-
highlighted_name =
|
|
333
|
+
highlighted_name = highlight_text(pattern, name.name, self.fuzzy)
|
|
331
334
|
matches.append(f'{p_parent}\\{p.name}::{name.parent}\\{highlighted_name}')
|
|
332
335
|
else: # content search
|
|
333
336
|
# Use dictionary: key: file path (colored), value: list of line matches
|
|
@@ -375,7 +378,7 @@ class Search:
|
|
|
375
378
|
|
|
376
379
|
count = pattern.count_matches(line) if isinstance(pattern, TermNode) else 0
|
|
377
380
|
# Highlight the matching parts in green
|
|
378
|
-
highlighted =
|
|
381
|
+
highlighted = highlight_text(pattern, line, self.fuzzy)
|
|
379
382
|
# Show a note if the pattern repeats 3 or more times
|
|
380
383
|
count_query = f' - Repeated {count} times' if count >= 3 else ''
|
|
381
384
|
# Format the output line with line number and highlighted matches
|
|
@@ -427,7 +430,7 @@ class Search:
|
|
|
427
430
|
return
|
|
428
431
|
count = pattern.count_matches(line_decoded) if isinstance(pattern, TermNode) else 0
|
|
429
432
|
# Highlight the matching parts in green
|
|
430
|
-
highlighted =
|
|
433
|
+
highlighted = highlight_text(pattern, line_decoded, self.fuzzy)
|
|
431
434
|
# Show a note if the pattern repeats 3 or more times
|
|
432
435
|
count_query = f' - Repeated {count} times' if count >= 3 else ''
|
|
433
436
|
# 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.
|
|
3
|
+
Version: 2.5.1
|
|
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
|
|
@@ -12,6 +12,7 @@ Requires-Dist: click==8.1.8
|
|
|
12
12
|
Requires-Dist: lark==1.2.2
|
|
13
13
|
Requires-Dist: py7zr==1.0.0
|
|
14
14
|
Requires-Dist: rarfile==4.2
|
|
15
|
+
Requires-Dist: rapidfuzz==3.13.0
|
|
15
16
|
Dynamic: license-file
|
|
16
17
|
|
|
17
18
|
# Pseek
|
|
@@ -43,7 +44,9 @@ pip install pseek
|
|
|
43
44
|
```sh
|
|
44
45
|
git clone https://github.com/ArianN8610/pysearch.git
|
|
45
46
|
cd pysearch
|
|
46
|
-
|
|
47
|
+
python -m venv venv
|
|
48
|
+
[Activate venv]
|
|
49
|
+
pip install click==8.1.8 lark==1.2.2 py7zr==1.0.0 rarfile==4.2 rapidfuzz==3.13.0
|
|
47
50
|
```
|
|
48
51
|
|
|
49
52
|
## Usage
|
|
@@ -81,28 +84,30 @@ pseek "error\d+" --regex
|
|
|
81
84
|
|
|
82
85
|
## Command Options
|
|
83
86
|
|
|
84
|
-
| Option | Description
|
|
85
|
-
|
|
86
|
-
| `--path` | Base directory to search in (default: current directory `.`)
|
|
87
|
-
| `--file` | Search only in file names
|
|
88
|
-
| `--directory` | Search only in directory names
|
|
89
|
-
| `--content` | Search inside file contents
|
|
90
|
-
| `--ext`, `--exclude-ext` | Filter by file extension (e.g., `txt`, `log`)
|
|
91
|
-
| `--case-sensitive` | Make the search case-sensitive (except when `--expr` is enabled, in which case you can make it case sensitive by putting `c` before term: `c"foo"`)
|
|
92
|
-
| `--regex` | Use regular expressions to search (except when `--expr` is enabled, in which case you can make it regex by putting `r` before term: `r"foo"`)
|
|
93
|
-
| `--include`, `--exclude` | Limit search results to specific set of directories or files
|
|
94
|
-
| `--re-include`, `--re-exclude` | Limit search results to specific directories or files with regex
|
|
95
|
-
| `--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"`)
|
|
96
|
-
| `--expr` | Enable to write conditions in the query. Example: `r"foo.*bar" and ("bar" or "baz") and not "qux"` (To use regex, word,
|
|
97
|
-
| `--timeout` | To stop the search after a specified period of time (Seconds)
|
|
98
|
-
| `--
|
|
99
|
-
| `--
|
|
100
|
-
| `--
|
|
101
|
-
| `--
|
|
102
|
-
| `--arc-
|
|
103
|
-
| `--
|
|
104
|
-
| `--
|
|
105
|
-
| `--
|
|
87
|
+
| Option | Description |
|
|
88
|
+
|--------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
89
|
+
| `--path` | Base directory to search in (default: current directory `.`) |
|
|
90
|
+
| `--file` | Search only in file names |
|
|
91
|
+
| `--directory` | Search only in directory names |
|
|
92
|
+
| `--content` | Search inside file contents |
|
|
93
|
+
| `--ext`, `--exclude-ext` | Filter by file extension (e.g., `txt`, `log`) |
|
|
94
|
+
| `--case-sensitive` | Make the search case-sensitive (except when `--expr` is enabled, in which case you can make it case sensitive by putting `c` before term: `c"foo"`) |
|
|
95
|
+
| `--regex` | Use regular expressions to search (except when `--expr` is enabled, in which case you can make it regex by putting `r` before term: `r"foo"`) |
|
|
96
|
+
| `--include`, `--exclude` | Limit search results to specific set of directories or files |
|
|
97
|
+
| `--re-include`, `--re-exclude` | Limit search results to specific directories or files with regex |
|
|
98
|
+
| `--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"`) |
|
|
99
|
+
| `--expr` | Enable to write conditions in the query. Example: `r"foo.*bar" and ("bar" or "baz") and not "qux"` (To use regex, word, case-sensitive, and fuzzy features, you can use the prefixes `r`, `w`, `c`, and `f` before terms. Allowed modes: `r`, `c`, `w`, `f`, `rc`, `cr`, `cw`, `wc`, `cf`, `fc`, `wf`, `fw`, `cwf`, `cfw`, `wcf`, `wfc`, `fcw`, `fwc`. Examples: `r"foo.*bar"`, `wcf"Aple"`, `cr".*Foo"`, ...) |
|
|
100
|
+
| `--timeout` | To stop the search after a specified period of time (Seconds) |
|
|
101
|
+
| `--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"` |
|
|
102
|
+
| `--fuzzy_level` | Similarity threshold from 0 to 99 for fuzzy search (default: `80`) |
|
|
103
|
+
| `--max-size`, `--min-size` | Specify maximum and minimum sizes for files and directories |
|
|
104
|
+
| `--archive` | Enable search within archive files (e.g. `zip`, `rar`, `7z`, `gz`, `bz2`, `xz`, `tar`, `tar.gz`, `tar.bz2`, `tar.xz`) |
|
|
105
|
+
| `--arc-ext`, `--arc-ee` | Filter by file extension inside archive files |
|
|
106
|
+
| `--arc-inc`, `--arc-exc` | Limit search results to specific set of directories or files inside archive files |
|
|
107
|
+
| `--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) |
|
|
108
|
+
| `--rarfb` | Path to RAR backend tool (e.g. UnRAR.exe, ...) |
|
|
109
|
+
| `--full-path` | Display full path of files and directories |
|
|
110
|
+
| `--no-content` | Only display files path for content search |
|
|
106
111
|
|
|
107
112
|
## Requirements
|
|
108
113
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pseek"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.5.1"
|
|
8
8
|
description = "Pseek is a Python library to search files, folders, and text"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -17,7 +17,8 @@ dependencies = [
|
|
|
17
17
|
"click==8.1.8",
|
|
18
18
|
"lark==1.2.2",
|
|
19
19
|
"py7zr==1.0.0",
|
|
20
|
-
"rarfile==4.2"
|
|
20
|
+
"rarfile==4.2",
|
|
21
|
+
"rapidfuzz==3.13.0"
|
|
21
22
|
]
|
|
22
23
|
urls = {Homepage = "https://github.com/ArianN8610/pysearch"}
|
|
23
24
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|