pseek 2.3.2__tar.gz → 2.5.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-2.3.2 → pseek-2.5.0}/PKG-INFO +37 -20
- {pseek-2.3.2 → pseek-2.5.0}/README.md +33 -19
- {pseek-2.3.2 → pseek-2.5.0}/pseek/cli.py +54 -5
- {pseek-2.3.2 → pseek-2.5.0}/pseek/parser.py +87 -23
- pseek-2.5.0/pseek/searcher.py +482 -0
- pseek-2.5.0/pseek/utils.py +87 -0
- {pseek-2.3.2 → pseek-2.5.0}/pseek.egg-info/PKG-INFO +37 -20
- pseek-2.5.0/pseek.egg-info/requires.txt +5 -0
- {pseek-2.3.2 → pseek-2.5.0}/pyproject.toml +5 -2
- pseek-2.3.2/pseek/searcher.py +0 -208
- pseek-2.3.2/pseek/utils.py +0 -12
- pseek-2.3.2/pseek.egg-info/requires.txt +0 -2
- {pseek-2.3.2 → pseek-2.5.0}/LICENSE +0 -0
- {pseek-2.3.2 → pseek-2.5.0}/pseek/__init__.py +0 -0
- {pseek-2.3.2 → pseek-2.5.0}/pseek.egg-info/SOURCES.txt +0 -0
- {pseek-2.3.2 → pseek-2.5.0}/pseek.egg-info/dependency_links.txt +0 -0
- {pseek-2.3.2 → pseek-2.5.0}/pseek.egg-info/entry_points.txt +0 -0
- {pseek-2.3.2 → pseek-2.5.0}/pseek.egg-info/top_level.txt +0 -0
- {pseek-2.3.2 → pseek-2.5.0}/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.0
|
|
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
|
|
@@ -10,6 +10,9 @@ Description-Content-Type: text/markdown
|
|
|
10
10
|
License-File: LICENSE
|
|
11
11
|
Requires-Dist: click==8.1.8
|
|
12
12
|
Requires-Dist: lark==1.2.2
|
|
13
|
+
Requires-Dist: py7zr==1.0.0
|
|
14
|
+
Requires-Dist: rarfile==4.2
|
|
15
|
+
Requires-Dist: rapidfuzz==3.13.0
|
|
13
16
|
Dynamic: license-file
|
|
14
17
|
|
|
15
18
|
# Pseek
|
|
@@ -24,17 +27,19 @@ A powerful command-line tool for searching files, directories, and content insid
|
|
|
24
27
|
* **Search inside file contents**
|
|
25
28
|
* **Highlight matches** in terminal output
|
|
26
29
|
* **Optimized for speed** with ThreadPoolExecutor
|
|
30
|
+
* Support **logical expression** for search queries
|
|
31
|
+
* Search inside **archive files** (e.g. `zip`, `rar`, `7z`, `gz`, `bz2`, `xz`, `tar`, `tar.gz`, `tar.bz2`, `tar.xz`)
|
|
27
32
|
* **Cross-platform** (Linux, macOS, Windows)
|
|
28
33
|
|
|
29
34
|
## Installation
|
|
30
35
|
|
|
31
|
-
###
|
|
36
|
+
### **`1` Install via `pip` (Recommended)**
|
|
32
37
|
|
|
33
38
|
```sh
|
|
34
39
|
pip install pseek
|
|
35
40
|
```
|
|
36
41
|
|
|
37
|
-
###
|
|
42
|
+
### **`2` Install from source**
|
|
38
43
|
|
|
39
44
|
```sh
|
|
40
45
|
git clone https://github.com/ArianN8610/pysearch.git
|
|
@@ -77,20 +82,32 @@ pseek "error\d+" --regex
|
|
|
77
82
|
|
|
78
83
|
## Command Options
|
|
79
84
|
|
|
80
|
-
| Option | Description
|
|
81
|
-
|
|
82
|
-
| `--path` | Base directory to search in (default: current directory `.`)
|
|
83
|
-
| `--file` | Search only in file names
|
|
84
|
-
| `--directory` | Search only in directory names
|
|
85
|
-
| `--content` | Search inside file contents
|
|
86
|
-
| `--ext`, `--exclude-ext` | Filter by file extension (e.g.,
|
|
87
|
-
| `--case-sensitive` | Make the search case-sensitive (except when
|
|
88
|
-
| `--regex` | Use regular expressions to search (except when
|
|
89
|
-
| `--include`, `--exclude` | Limit search results to specific set of directories or files
|
|
90
|
-
| `--re-include`, `--re-exclude` | Limit search results to specific directories or files with regex
|
|
91
|
-
| `--word` | Match the whole word only (except when
|
|
92
|
-
| `--expr` | Enable to write conditions in the query. Example: r"foo.*bar" and ("bar" or "baz") and not "qux" (To use regex, word, and case-sensitive features, you can use the prefixes r
|
|
93
|
-
| `--timeout` | To stop the search after a specified period of time (Seconds)
|
|
94
|
-
| `--
|
|
95
|
-
| `--
|
|
96
|
-
| `--
|
|
85
|
+
| Option | Description |
|
|
86
|
+
|--------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
87
|
+
| `--path` | Base directory to search in (default: current directory `.`) |
|
|
88
|
+
| `--file` | Search only in file names |
|
|
89
|
+
| `--directory` | Search only in directory names |
|
|
90
|
+
| `--content` | Search inside file contents |
|
|
91
|
+
| `--ext`, `--exclude-ext` | Filter by file extension (e.g., `txt`, `log`) |
|
|
92
|
+
| `--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"`) |
|
|
93
|
+
| `--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"`) |
|
|
94
|
+
| `--include`, `--exclude` | Limit search results to specific set of directories or files |
|
|
95
|
+
| `--re-include`, `--re-exclude` | Limit search results to specific directories or files with regex |
|
|
96
|
+
| `--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"`) |
|
|
97
|
+
| `--expr` | Enable to write conditions in the query. Example: `r"foo.*bar" and ("bar" or "baz") and not "qux"` (To use regex, word, and case-sensitive features, you can use the prefixes `r`, `w`, and `c` before terms. Allowed modes: `r`, `w`, `c`, `wc`, `cw`, `rc`, `cr`. Examples: `r"foo.*bar"`, `wc"Foo"`, `cr".*Foo"`, ...) |
|
|
98
|
+
| `--timeout` | To stop the search after a specified period of time (Seconds) |
|
|
99
|
+
| `--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) |
|
|
100
|
+
| `--fuzzy_level` | Similarity threshold from 0 to 99 for fuzzy search (default: `80`) |
|
|
101
|
+
| `--max-size`, `--min-size` | Specify maximum and minimum sizes for files and directories |
|
|
102
|
+
| `--archive` | Enable search within archive files (e.g. `zip`, `rar`, `7z`, `gz`, `bz2`, `xz`, `tar`, `tar.gz`, `tar.bz2`, `tar.xz`) |
|
|
103
|
+
| `--arc-ext`, `--arc-ee` | Filter by file extension inside archive files |
|
|
104
|
+
| `--arc-inc`, `--arc-exc` | Limit search results to specific set of directories or files inside archive files |
|
|
105
|
+
| `--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) |
|
|
106
|
+
| `--rarfb` | Path to RAR backend tool (e.g. UnRAR.exe, ...) |
|
|
107
|
+
| `--full-path` | Display full path of files and directories |
|
|
108
|
+
| `--no-content` | Only display files path for content search |
|
|
109
|
+
|
|
110
|
+
## Requirements
|
|
111
|
+
|
|
112
|
+
* Python 3.6 or higher
|
|
113
|
+
* `unrar`, `bsdtar`, `unar` or `7zip` for the rarfile library to support searching inside `.rar` files (optional)
|
|
@@ -10,17 +10,19 @@ A powerful command-line tool for searching files, directories, and content insid
|
|
|
10
10
|
* **Search inside file contents**
|
|
11
11
|
* **Highlight matches** in terminal output
|
|
12
12
|
* **Optimized for speed** with ThreadPoolExecutor
|
|
13
|
+
* Support **logical expression** for search queries
|
|
14
|
+
* Search inside **archive files** (e.g. `zip`, `rar`, `7z`, `gz`, `bz2`, `xz`, `tar`, `tar.gz`, `tar.bz2`, `tar.xz`)
|
|
13
15
|
* **Cross-platform** (Linux, macOS, Windows)
|
|
14
16
|
|
|
15
17
|
## Installation
|
|
16
18
|
|
|
17
|
-
###
|
|
19
|
+
### **`1` Install via `pip` (Recommended)**
|
|
18
20
|
|
|
19
21
|
```sh
|
|
20
22
|
pip install pseek
|
|
21
23
|
```
|
|
22
24
|
|
|
23
|
-
###
|
|
25
|
+
### **`2` Install from source**
|
|
24
26
|
|
|
25
27
|
```sh
|
|
26
28
|
git clone https://github.com/ArianN8610/pysearch.git
|
|
@@ -63,20 +65,32 @@ pseek "error\d+" --regex
|
|
|
63
65
|
|
|
64
66
|
## Command Options
|
|
65
67
|
|
|
66
|
-
| Option | Description
|
|
67
|
-
|
|
68
|
-
| `--path` | Base directory to search in (default: current directory `.`)
|
|
69
|
-
| `--file` | Search only in file names
|
|
70
|
-
| `--directory` | Search only in directory names
|
|
71
|
-
| `--content` | Search inside file contents
|
|
72
|
-
| `--ext`, `--exclude-ext` | Filter by file extension (e.g.,
|
|
73
|
-
| `--case-sensitive` | Make the search case-sensitive (except when
|
|
74
|
-
| `--regex` | Use regular expressions to search (except when
|
|
75
|
-
| `--include`, `--exclude` | Limit search results to specific set of directories or files
|
|
76
|
-
| `--re-include`, `--re-exclude` | Limit search results to specific directories or files with regex
|
|
77
|
-
| `--word` | Match the whole word only (except when
|
|
78
|
-
| `--expr` | Enable to write conditions in the query. Example: r"foo.*bar" and ("bar" or "baz") and not "qux" (To use regex, word, and case-sensitive features, you can use the prefixes r
|
|
79
|
-
| `--timeout` | To stop the search after a specified period of time (Seconds)
|
|
80
|
-
| `--
|
|
81
|
-
| `--
|
|
82
|
-
| `--
|
|
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, and case-sensitive features, you can use the prefixes `r`, `w`, and `c` before terms. Allowed modes: `r`, `w`, `c`, `wc`, `cw`, `rc`, `cr`. Examples: `r"foo.*bar"`, `wc"Foo"`, `cr".*Foo"`, ...) |
|
|
81
|
+
| `--timeout` | To stop the search after a specified period of time (Seconds) |
|
|
82
|
+
| `--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) |
|
|
83
|
+
| `--fuzzy_level` | Similarity threshold from 0 to 99 for fuzzy search (default: `80`) |
|
|
84
|
+
| `--max-size`, `--min-size` | Specify maximum and minimum sizes for files and directories |
|
|
85
|
+
| `--archive` | Enable search within archive files (e.g. `zip`, `rar`, `7z`, `gz`, `bz2`, `xz`, `tar`, `tar.gz`, `tar.bz2`, `tar.xz`) |
|
|
86
|
+
| `--arc-ext`, `--arc-ee` | Filter by file extension inside archive files |
|
|
87
|
+
| `--arc-inc`, `--arc-exc` | Limit search results to specific set of directories or files inside archive files |
|
|
88
|
+
| `--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) |
|
|
89
|
+
| `--rarfb` | Path to RAR backend tool (e.g. UnRAR.exe, ...) |
|
|
90
|
+
| `--full-path` | Display full path of files and directories |
|
|
91
|
+
| `--no-content` | Only display files path for content search |
|
|
92
|
+
|
|
93
|
+
## Requirements
|
|
94
|
+
|
|
95
|
+
* Python 3.6 or higher
|
|
96
|
+
* `unrar`, `bsdtar`, `unar` or `7zip` for the rarfile library to support searching inside `.rar` files (optional)
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import click
|
|
2
2
|
from .searcher import Search
|
|
3
3
|
from multiprocessing import Process
|
|
4
|
+
from .utils import check_rar_backend
|
|
4
5
|
|
|
5
6
|
|
|
6
|
-
def run_search_process(file, directory, content,
|
|
7
|
+
def run_search_process(file, directory, content, search_instance):
|
|
7
8
|
"""Performs the basic search operation"""
|
|
8
9
|
total_results = 0
|
|
9
10
|
|
|
@@ -11,7 +12,7 @@ def run_search_process(file, directory, content, ext, exclude_ext, search_instan
|
|
|
11
12
|
if file:
|
|
12
13
|
total_results += search_instance.search('file').echo('Files', 'file')
|
|
13
14
|
# Search for directories if requested and extension filters are not active.
|
|
14
|
-
if directory
|
|
15
|
+
if directory:
|
|
15
16
|
total_results += search_instance.search('directory').echo('Directories', 'directory')
|
|
16
17
|
# Search for content inside files if requested.
|
|
17
18
|
if content:
|
|
@@ -22,7 +23,6 @@ def run_search_process(file, directory, content, ext, exclude_ext, search_instan
|
|
|
22
23
|
click.echo(click.style(message, fg='red'))
|
|
23
24
|
|
|
24
25
|
|
|
25
|
-
|
|
26
26
|
@click.command()
|
|
27
27
|
@click.argument('query')
|
|
28
28
|
@click.option('-p', '--path', type=click.Path(exists=True, file_okay=False, dir_okay=True),
|
|
@@ -51,6 +51,9 @@ def run_search_process(file, directory, content, ext, exclude_ext, search_instan
|
|
|
51
51
|
'Examples: r"foo.*bar", wc"Foo", cr".*Foo", ...)')
|
|
52
52
|
@click.option('--timeout', type=click.INT,
|
|
53
53
|
help='To stop the search after a specified period of time (Seconds)')
|
|
54
|
+
@click.option('--fuzzy', is_flag=True, help='Enable fuzzy search (approximate matching).')
|
|
55
|
+
@click.option('--fuzzy-level', type=click.IntRange(0, 99), default=80, show_default=True,
|
|
56
|
+
help='Similarity threshold from 0 to 99 for fuzzy search.')
|
|
54
57
|
# Extension filters
|
|
55
58
|
@click.option('--ext', multiple=True, type=click.STRING,
|
|
56
59
|
help='Include files with these extensions. Example: --ext py --ext js')
|
|
@@ -68,12 +71,49 @@ def run_search_process(file, directory, content, ext, exclude_ext, search_instan
|
|
|
68
71
|
# Size filters
|
|
69
72
|
@click.option('--max-size', type=click.FLOAT, help='Maximum file/directory size (in MB).')
|
|
70
73
|
@click.option('--min-size', type=click.FLOAT, help='Minimum file/directory size (in MB).')
|
|
74
|
+
# Archive options
|
|
75
|
+
@click.option('--archive', is_flag=True,
|
|
76
|
+
help='Enable search within archive files (e.g. zip, rar, 7z, gz, bz2, xz, tar, tar.gz, tar.bz2, tar.xz)')
|
|
77
|
+
@click.option('--arc-ext', multiple=True, type=click.STRING,
|
|
78
|
+
help='Include files with these extensions inside archive files. Example: --arc-ext py --arc-ext js')
|
|
79
|
+
@click.option('--arc-ee', multiple=True, type=click.STRING,
|
|
80
|
+
help='Exclude files with these extensions inside archive files. Example: --arc-ee jpg --arc-ee exe')
|
|
81
|
+
@click.option('--arc-inc', type=click.Path(file_okay=True, dir_okay=True),
|
|
82
|
+
multiple=True, help='Directories or files to include in search for inside archive files.')
|
|
83
|
+
@click.option('--arc-exc', type=click.Path(file_okay=True, dir_okay=True),
|
|
84
|
+
multiple=True, help='Directories or files to exclude from search for inside archive files.')
|
|
85
|
+
@click.option('--arc-max', type=click.FLOAT, help='Maximum size of files in the archive (in MB).')
|
|
86
|
+
@click.option('--arc-min', type=click.FLOAT, help='Minimum size of files in the archive (in MB).')
|
|
87
|
+
@click.option('--rarfb', type=click.Path(exists=True, file_okay=True, dir_okay=False),
|
|
88
|
+
help='Path to RAR backend tool (e.g. UnRAR.exe, ...). '
|
|
89
|
+
'Enter the file type in the query (e.g. unrar, bsdtar, unar, 7z).')
|
|
71
90
|
# Output option
|
|
72
91
|
@click.option('--full-path', is_flag=True, help='Display full paths for results.')
|
|
73
92
|
@click.option('--no-content', is_flag=True, help='Only display files path for content search.')
|
|
74
93
|
def search(query, path, file, directory, content, case_sensitive, ext, exclude_ext, regex, include, exclude,
|
|
75
|
-
re_include, re_exclude, word, expr, timeout, max_size, min_size,
|
|
94
|
+
re_include, re_exclude, word, expr, timeout, fuzzy, fuzzy_level, max_size, min_size, archive, arc_ext,
|
|
95
|
+
arc_ee, arc_inc, arc_exc, arc_max, arc_min, rarfb, full_path, no_content):
|
|
76
96
|
"""Search for files, directories, and file content based on the query."""
|
|
97
|
+
|
|
98
|
+
check_rar_backend(archive, rarfb, query)
|
|
99
|
+
|
|
100
|
+
if not expr and fuzzy:
|
|
101
|
+
if not word:
|
|
102
|
+
click.echo(
|
|
103
|
+
click.style(
|
|
104
|
+
"Warning: Fuzzy substring highlighting and counting matches are disabled to improve performance.\n",
|
|
105
|
+
fg="yellow"
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
else:
|
|
109
|
+
click.echo(
|
|
110
|
+
click.style(
|
|
111
|
+
'Warning: When using "--fuzzy" and "--word", it is better to have the query be a word and '
|
|
112
|
+
'not a phrase, as this will cause errors in the results.\n',
|
|
113
|
+
fg="yellow"
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
|
|
77
117
|
# If no search type is specified, search in all types.
|
|
78
118
|
if not any((file, directory, content)):
|
|
79
119
|
file = directory = content = True
|
|
@@ -92,8 +132,17 @@ def search(query, path, file, directory, content, case_sensitive, ext, exclude_e
|
|
|
92
132
|
re_exclude=re_exclude,
|
|
93
133
|
whole_word=word,
|
|
94
134
|
expr=expr,
|
|
135
|
+
fuzzy=fuzzy,
|
|
136
|
+
fuzzy_level=fuzzy_level,
|
|
95
137
|
max_size=max_size,
|
|
96
138
|
min_size=min_size,
|
|
139
|
+
archive=archive,
|
|
140
|
+
arc_ext=arc_ext,
|
|
141
|
+
arc_ee=arc_ee,
|
|
142
|
+
arc_inc=arc_inc,
|
|
143
|
+
arc_exc=arc_exc,
|
|
144
|
+
arc_max=arc_max,
|
|
145
|
+
arc_min=arc_min,
|
|
97
146
|
full_path=full_path,
|
|
98
147
|
no_content=no_content
|
|
99
148
|
)
|
|
@@ -112,7 +161,7 @@ def search(query, path, file, directory, content, case_sensitive, ext, exclude_e
|
|
|
112
161
|
p.join()
|
|
113
162
|
click.echo(click.style(f"\nTimeout! Search exceeded {timeout} seconds and was stopped.", fg="red"))
|
|
114
163
|
else:
|
|
115
|
-
run_search_process(file, directory, content,
|
|
164
|
+
run_search_process(file, directory, content, search_instance)
|
|
116
165
|
|
|
117
166
|
|
|
118
167
|
if __name__ == "__main__":
|
|
@@ -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
|
|
|
@@ -99,8 +133,10 @@ 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, fuzzy_level):
|
|
103
137
|
super().__init__()
|
|
138
|
+
self.fuzzy = fuzzy
|
|
139
|
+
self.fuzzy_level = fuzzy_level
|
|
104
140
|
|
|
105
141
|
def string(self, s):
|
|
106
142
|
""" Match normal quoted string: "foo" """
|
|
@@ -109,7 +145,9 @@ class TreeToExpr(Transformer):
|
|
|
109
145
|
term,
|
|
110
146
|
False,
|
|
111
147
|
False,
|
|
112
|
-
False
|
|
148
|
+
False,
|
|
149
|
+
self.fuzzy,
|
|
150
|
+
self.fuzzy_level
|
|
113
151
|
)
|
|
114
152
|
|
|
115
153
|
def prefixed_string(self, s):
|
|
@@ -122,6 +160,8 @@ class TreeToExpr(Transformer):
|
|
|
122
160
|
regex='r' in prefix,
|
|
123
161
|
whole_word='w' in prefix,
|
|
124
162
|
case_sensitive='c' in prefix,
|
|
163
|
+
fuzzy=self.fuzzy,
|
|
164
|
+
fuzzy_level=self.fuzzy_level
|
|
125
165
|
)
|
|
126
166
|
|
|
127
167
|
def and_expr(self, args):
|
|
@@ -134,48 +174,72 @@ class TreeToExpr(Transformer):
|
|
|
134
174
|
return NotNode(args[0])
|
|
135
175
|
|
|
136
176
|
|
|
137
|
-
def parse_query_expression(query: str, expr, regex, whole_word, case_sensitive) -> ExprNode:
|
|
177
|
+
def parse_query_expression(query: str, expr, regex, whole_word, case_sensitive, fuzzy, fuzzy_level) -> ExprNode:
|
|
138
178
|
"""
|
|
139
179
|
Function to parse the query and return expression tree.
|
|
140
180
|
If expr is False, treat the whole query as a single term.
|
|
141
181
|
"""
|
|
142
182
|
|
|
143
183
|
if not expr:
|
|
144
|
-
return TermNode(query, regex, whole_word, case_sensitive)
|
|
184
|
+
return TermNode(query, regex, whole_word, case_sensitive, fuzzy, fuzzy_level)
|
|
145
185
|
|
|
146
186
|
# Otherwise, parse using Lark
|
|
147
187
|
parser = Lark(query_grammar, parser="lalr")
|
|
148
188
|
try:
|
|
149
189
|
tree = parser.parse(query)
|
|
150
|
-
return TreeToExpr().transform(tree)
|
|
190
|
+
return TreeToExpr(fuzzy, fuzzy_level).transform(tree)
|
|
151
191
|
except Exception as e:
|
|
152
192
|
click.echo(click.style("Query parser error:\n\n", fg='red') + str(e))
|
|
153
193
|
sys.exit(1)
|
|
154
194
|
|
|
155
195
|
|
|
156
|
-
def
|
|
196
|
+
def highlight_text(expr: ExprNode, text: str, fuzzy: bool) -> str:
|
|
197
|
+
"""
|
|
198
|
+
Highlight matching parts of the text.
|
|
199
|
+
Only highlights fuzzy matches when whole_word=True.
|
|
200
|
+
"""
|
|
157
201
|
matches = []
|
|
158
202
|
|
|
159
203
|
def collect_matches(node):
|
|
160
204
|
if isinstance(node, TermNode):
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
205
|
+
# Skip fuzzy highlight if whole_word is False
|
|
206
|
+
if node.fuzzy:
|
|
207
|
+
if not node.whole_word:
|
|
208
|
+
return # skip highlighting
|
|
209
|
+
text_cmp = text if node.case_sensitive else text.lower()
|
|
210
|
+
term = node.raw_term if node.case_sensitive else node.raw_term.lower()
|
|
211
|
+
|
|
212
|
+
# collect word matches
|
|
213
|
+
for match in re.finditer(r'\w+', text_cmp):
|
|
214
|
+
word = match.group()
|
|
215
|
+
if fuzz.ratio(term, word) >= node.fuzzy_level:
|
|
216
|
+
matches.append((match.start(), match.end()))
|
|
217
|
+
else:
|
|
218
|
+
for match in node.pattern.finditer(text):
|
|
219
|
+
matches.append((match.start(), match.end()))
|
|
220
|
+
elif isinstance(node, AndNode) or isinstance(node, OrNode):
|
|
164
221
|
collect_matches(node.left)
|
|
165
222
|
collect_matches(node.right)
|
|
166
223
|
elif isinstance(node, NotNode):
|
|
167
224
|
collect_matches(node.child)
|
|
168
225
|
|
|
226
|
+
# If fuzzy is enabled but whole_word is False, return plain text
|
|
227
|
+
if fuzzy and isinstance(expr, TermNode) and not expr.whole_word:
|
|
228
|
+
return text
|
|
229
|
+
|
|
169
230
|
collect_matches(expr)
|
|
170
231
|
|
|
171
|
-
#
|
|
232
|
+
# Sort and merge overlapping matches (for example, if one match was inside another match)
|
|
172
233
|
matches.sort() # sort by start position
|
|
173
234
|
merged = []
|
|
174
235
|
for start, end in matches:
|
|
175
236
|
if not merged or start >= merged[-1][1]: # no overlap
|
|
176
237
|
merged.append((start, end))
|
|
238
|
+
else:
|
|
239
|
+
# Merge overlapping
|
|
240
|
+
merged[-1] = (merged[-1][0], max(merged[-1][1], end))
|
|
177
241
|
|
|
178
|
-
#
|
|
242
|
+
# Build highlighted text
|
|
179
243
|
result = []
|
|
180
244
|
last = 0
|
|
181
245
|
for start, end in merged:
|