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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pseek
3
- Version: 2.4.0
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
- pip install click==8.1.8 lark==1.2.2
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, 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"`, ...) |
97
- | `--timeout` | To stop the search after a specified period of time (Seconds) |
98
- | `--max-size`, `--min-size` | Specify maximum and minimum sizes for files and directories |
99
- | `--archive` | Enable search within archive files (e.g. `zip`, `rar`, `7z`, `gz`, `bz2`, `xz`, `tar`, `tar.gz`, `tar.bz2`, `tar.xz`) |
100
- | `--arc-ext`, `--arc-ee` | Filter by file extension inside archive files |
101
- | `--arc-inc`, `--arc-exc` | Limit search results to specific set of directories or files inside archive files |
102
- | `--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) |
103
- | `--rarfb` | Path to RAR backend tool (e.g. UnRAR.exe, ...) |
104
- | `--full-path` | Display full path of files and directories |
105
- | `--no-content` | Only display files path for content search |
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
- pip install click==8.1.8 lark==1.2.2
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, 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
- | `--max-size`, `--min-size` | Specify maximum and minimum sizes for files and directories |
83
- | `--archive` | Enable search within archive files (e.g. `zip`, `rar`, `7z`, `gz`, `bz2`, `xz`, `tar`, `tar.gz`, `tar.bz2`, `tar.xz`) |
84
- | `--arc-ext`, `--arc-ee` | Filter by file extension inside archive files |
85
- | `--arc-inc`, `--arc-exc` | Limit search results to specific set of directories or files inside archive files |
86
- | `--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) |
87
- | `--rarfb` | Path to RAR backend tool (e.g. UnRAR.exe, ...) |
88
- | `--full-path` | Display full path of files and directories |
89
- | `--no-content` | Only display files path for content search |
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, and case-sensitive features, '
51
- 'you can use the prefixes r, w, and c before terms. Allowed modes: r, w, c, wc, cw, rc, cr. '
52
- 'Examples: r"foo.*bar", wc"Foo", cr".*Foo", ...)')
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, arc_ee, arc_inc,
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
- flags = 0 if case_sensitive else re.IGNORECASE # Adjust case sensitivity
24
+ if not fuzzy:
25
+ flags = 0 if case_sensitive else re.IGNORECASE # Adjust case sensitivity
22
26
 
23
- # Build the regex pattern
24
- if not regex:
25
- term = re.escape(term) # Escape if not regex
26
- # Apply whole-word matching only if not using regex (or if desired behavior is defined)
27
- if whole_word:
28
- term = r'\b' + term + r'\b'
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
- self.pattern = compile_regex(term, flags) # Precompile the regex pattern for performance
34
+ self.pattern = compile_regex(term, flags) # Precompile the regex pattern for performance
31
35
 
32
36
  def evaluate(self, text: str) -> bool:
33
- return bool(self.pattern.search(text))
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
- return len(list(self.pattern.finditer(text)))
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 highlight_text_safe(expr: ExprNode, text: str) -> str:
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
- for match in node.pattern.finditer(text):
162
- matches.append((match.start(), match.end()))
163
- elif isinstance(node, (AndNode, OrNode)):
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
- # Remove overlaps (for example, if one match was inside another match)
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
- # Create the final text with highlight
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, highlight_text_safe
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, arc_inc, arc_exc,
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 = highlight_text_safe(pattern, p.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 = highlight_text_safe(pattern, name.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 = highlight_text_safe(pattern, line)
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 = highlight_text_safe(pattern, line_decoded)
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.4.0
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
- pip install click==8.1.8 lark==1.2.2
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, 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"`, ...) |
97
- | `--timeout` | To stop the search after a specified period of time (Seconds) |
98
- | `--max-size`, `--min-size` | Specify maximum and minimum sizes for files and directories |
99
- | `--archive` | Enable search within archive files (e.g. `zip`, `rar`, `7z`, `gz`, `bz2`, `xz`, `tar`, `tar.gz`, `tar.bz2`, `tar.xz`) |
100
- | `--arc-ext`, `--arc-ee` | Filter by file extension inside archive files |
101
- | `--arc-inc`, `--arc-exc` | Limit search results to specific set of directories or files inside archive files |
102
- | `--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) |
103
- | `--rarfb` | Path to RAR backend tool (e.g. UnRAR.exe, ...) |
104
- | `--full-path` | Display full path of files and directories |
105
- | `--no-content` | Only display files path for content search |
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
 
@@ -2,3 +2,4 @@ click==8.1.8
2
2
  lark==1.2.2
3
3
  py7zr==1.0.0
4
4
  rarfile==4.2
5
+ rapidfuzz==3.13.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pseek"
7
- version = "2.4.0"
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