pseek 1.0.2__tar.gz → 2.0.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-1.0.2 → pseek-2.0.0}/PKG-INFO +1 -1
- {pseek-1.0.2 → pseek-2.0.0}/pseek/cli.py +8 -25
- pseek-2.0.0/pseek/searcher.py +146 -0
- {pseek-1.0.2 → pseek-2.0.0}/pseek.egg-info/PKG-INFO +1 -1
- {pseek-1.0.2 → pseek-2.0.0}/pseek.egg-info/SOURCES.txt +0 -1
- {pseek-1.0.2 → pseek-2.0.0}/setup.py +1 -1
- pseek-1.0.2/pseek/searcher.py +0 -109
- pseek-1.0.2/pseek/utils.py +0 -66
- {pseek-1.0.2 → pseek-2.0.0}/LICENSE +0 -0
- {pseek-1.0.2 → pseek-2.0.0}/README.md +0 -0
- {pseek-1.0.2 → pseek-2.0.0}/pseek/__init__.py +0 -0
- {pseek-1.0.2 → pseek-2.0.0}/pseek.egg-info/dependency_links.txt +0 -0
- {pseek-1.0.2 → pseek-2.0.0}/pseek.egg-info/entry_points.txt +0 -0
- {pseek-1.0.2 → pseek-2.0.0}/pseek.egg-info/requires.txt +0 -0
- {pseek-1.0.2 → pseek-2.0.0}/pseek.egg-info/top_level.txt +0 -0
- {pseek-1.0.2 → pseek-2.0.0}/setup.cfg +0 -0
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import re
|
|
2
1
|
import click
|
|
3
|
-
|
|
4
|
-
from .utils import display_results
|
|
5
|
-
from .searcher import search_in_names, search_in_file_contents
|
|
2
|
+
from .searcher import Search
|
|
6
3
|
|
|
7
4
|
|
|
8
5
|
@click.command()
|
|
@@ -28,36 +25,22 @@ def search(query, path, file, directory, content, case_sensitive, ext, regex, in
|
|
|
28
25
|
max_size, min_size, full_path):
|
|
29
26
|
"""Search for files, directories, and content based on the query."""
|
|
30
27
|
|
|
31
|
-
if regex:
|
|
32
|
-
try:
|
|
33
|
-
re.compile(query) # Ensure valid regex
|
|
34
|
-
except re.error:
|
|
35
|
-
click.echo(click.style('Invalid regex pattern: ', fg='red') + query)
|
|
36
|
-
return
|
|
37
|
-
|
|
38
28
|
# Enable all search types if none are explicitly selected
|
|
39
29
|
if not any([file, directory, content]):
|
|
40
30
|
file = directory = content = True
|
|
41
31
|
|
|
42
|
-
|
|
32
|
+
count_results = 0
|
|
33
|
+
search_class = Search(path, query, case_sensitive, ext, regex, include, exclude, word, max_size, min_size,
|
|
34
|
+
full_path)
|
|
43
35
|
|
|
44
36
|
if file:
|
|
45
|
-
|
|
46
|
-
min_size, full_path, ext)
|
|
47
|
-
display_results(filename_results, 'Files', 'file')
|
|
48
|
-
results.extend(filename_results)
|
|
37
|
+
count_results += search_class.search('file').echo('Files', 'file')
|
|
49
38
|
if directory and not ext:
|
|
50
|
-
|
|
51
|
-
full_path, is_file=False)
|
|
52
|
-
display_results(dir_results, 'Directories', 'directory')
|
|
53
|
-
results.extend(dir_results)
|
|
39
|
+
count_results += search_class.search('directory').echo('Directories', 'directory')
|
|
54
40
|
if content:
|
|
55
|
-
|
|
56
|
-
max_size, min_size, full_path)
|
|
57
|
-
display_results(content_results, 'Contents', 'content')
|
|
58
|
-
results.extend(content_results)
|
|
41
|
+
count_results += search_class.search('content').echo('Contents', 'content')
|
|
59
42
|
|
|
60
|
-
message = f'\nTotal results: {
|
|
43
|
+
message = f'\nTotal results: {count_results}' if count_results else 'No results found'
|
|
61
44
|
click.echo(click.style(message, fg='red')) # Display total of results
|
|
62
45
|
|
|
63
46
|
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import sys
|
|
3
|
+
import click
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Search:
|
|
9
|
+
def __init__(self, base_path, query, case_sensitive, ext, regex, include, exclude, whole_word,
|
|
10
|
+
max_size, min_size, full_path):
|
|
11
|
+
self.base_path = base_path
|
|
12
|
+
self.query = query
|
|
13
|
+
self.case_sensitive = case_sensitive
|
|
14
|
+
self.ext = ext
|
|
15
|
+
self.regex = regex
|
|
16
|
+
self.include = include
|
|
17
|
+
self.exclude = exclude
|
|
18
|
+
self.whole_word = whole_word
|
|
19
|
+
self.max_size = max_size
|
|
20
|
+
self.min_size = min_size
|
|
21
|
+
self.full_path = full_path
|
|
22
|
+
|
|
23
|
+
self.result = []
|
|
24
|
+
|
|
25
|
+
def conditions(self, p_resolved: Path, search_type: str) -> bool:
|
|
26
|
+
"""Check the correctness of path based on data"""
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
p_size_mb = p_resolved.stat().st_size / 1_048_576 # Convert to MB
|
|
30
|
+
except OSError:
|
|
31
|
+
return True # if path cannot be accessible
|
|
32
|
+
|
|
33
|
+
# Convert include and exclude paths to `Path`
|
|
34
|
+
include_paths = {Path(p).resolve() for p in self.include}
|
|
35
|
+
exclude_paths = {Path(p).resolve() for p in self.exclude}
|
|
36
|
+
|
|
37
|
+
if (
|
|
38
|
+
# Filter by include/exclude directories and files
|
|
39
|
+
(self.include and not any(p_resolved.is_relative_to(inc) for inc in include_paths))
|
|
40
|
+
or (self.exclude and any(p_resolved.is_relative_to(exc) for exc in exclude_paths))
|
|
41
|
+
# Filter by extension
|
|
42
|
+
or (self.ext and p_resolved.suffix[1:] not in self.ext)
|
|
43
|
+
# Filter by file size
|
|
44
|
+
or (self.max_size and p_size_mb > self.max_size)
|
|
45
|
+
or (self.min_size and p_size_mb < self.min_size)
|
|
46
|
+
# Filter by path type
|
|
47
|
+
or ((search_type == 'file' or search_type == 'content') and not p_resolved.is_file())
|
|
48
|
+
or (search_type == 'directory' and not p_resolved.is_dir())
|
|
49
|
+
):
|
|
50
|
+
return True
|
|
51
|
+
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
def search(self, search_type: str):
|
|
55
|
+
"""Search for paths based on the query"""
|
|
56
|
+
|
|
57
|
+
base_path = Path(self.base_path)
|
|
58
|
+
query = self.query
|
|
59
|
+
|
|
60
|
+
if not self.regex:
|
|
61
|
+
query = re.escape(query) # If regex is disabled, convert query to plain text
|
|
62
|
+
else:
|
|
63
|
+
try:
|
|
64
|
+
re.compile(query) # Ensure valid regex
|
|
65
|
+
except re.error:
|
|
66
|
+
click.echo(click.style('Invalid regex pattern: ', fg='red') + query)
|
|
67
|
+
sys.exit(1) # Exit the program with error code
|
|
68
|
+
|
|
69
|
+
if self.whole_word:
|
|
70
|
+
query = rf'\b{query}\b' # Ensure search for whole words
|
|
71
|
+
|
|
72
|
+
flags = 0 if self.case_sensitive else re.IGNORECASE # Adjust case sensitivity
|
|
73
|
+
|
|
74
|
+
if search_type == 'file' or search_type == 'directory':
|
|
75
|
+
matches = []
|
|
76
|
+
for p in base_path.rglob('*'):
|
|
77
|
+
p_resolved = p.resolve() # Actual file or folder path
|
|
78
|
+
if self.conditions(p_resolved, search_type) or (not re.search(query, p.name, flags)):
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
# Specify the found part
|
|
82
|
+
highlighted_name = re.sub(query, lambda m: click.style(m.group(), fg='green'), p.name, flags=flags)
|
|
83
|
+
# Get full path if full_path is True
|
|
84
|
+
p_parent = p_resolved.parent if self.full_path else p.parent
|
|
85
|
+
|
|
86
|
+
matches.append(f'{p_parent}\\{highlighted_name}')
|
|
87
|
+
else:
|
|
88
|
+
matches = dict()
|
|
89
|
+
def process_file(file_path):
|
|
90
|
+
"""Processes a single file and searches for the query inside its content."""
|
|
91
|
+
p_resolved = file_path.resolve()
|
|
92
|
+
line_matches = []
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
for num, line in enumerate(p_resolved.read_text(encoding='utf-8', errors='ignore').splitlines(), 1):
|
|
96
|
+
line = line.strip()
|
|
97
|
+
if re.search(query, line, flags):
|
|
98
|
+
# Count query in each line
|
|
99
|
+
count_query = len(list(re.finditer(query, line, flags)))
|
|
100
|
+
highlighted_name = re.sub(query, lambda m: click.style(m.group(), fg='green'), line,
|
|
101
|
+
flags=flags)
|
|
102
|
+
count_query = f'- Repeated {count_query} times' if count_query >= 3 else ''
|
|
103
|
+
|
|
104
|
+
line_matches.append(
|
|
105
|
+
click.style(f'Line {num}{count_query}: ', fg='magenta')
|
|
106
|
+
+ highlighted_name
|
|
107
|
+
)
|
|
108
|
+
except Exception:
|
|
109
|
+
return # Ignore unreadable files
|
|
110
|
+
|
|
111
|
+
if line_matches:
|
|
112
|
+
file_path = p_resolved if self.full_path else file_path
|
|
113
|
+
matches.update({click.style(file_path, fg='cyan'): line_matches})
|
|
114
|
+
|
|
115
|
+
# Use multi-threading for faster processing
|
|
116
|
+
with ThreadPoolExecutor() as executor:
|
|
117
|
+
executor.map(
|
|
118
|
+
process_file,
|
|
119
|
+
(file for file in base_path.rglob('*') if not self.conditions(file, 'content'))
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
self.result = matches
|
|
123
|
+
return self
|
|
124
|
+
|
|
125
|
+
def echo(self, title: str, result_name: str) -> int:
|
|
126
|
+
"""Display search results"""
|
|
127
|
+
count_result = 0
|
|
128
|
+
|
|
129
|
+
if self.result:
|
|
130
|
+
click.echo(click.style(f'\n{title}:\n', fg='yellow'))
|
|
131
|
+
|
|
132
|
+
if isinstance(self.result, dict):
|
|
133
|
+
# Display for content results
|
|
134
|
+
for key, value in self.result.items():
|
|
135
|
+
click.echo(key)
|
|
136
|
+
click.echo('\n'.join(value) + '\n')
|
|
137
|
+
count_result += len(value)
|
|
138
|
+
else:
|
|
139
|
+
# Display for directory and file results
|
|
140
|
+
count_result = len(self.result)
|
|
141
|
+
click.echo('\n'.join(self.result))
|
|
142
|
+
|
|
143
|
+
if count_result >= 3:
|
|
144
|
+
click.echo(click.style(f'\n{count_result} results found for {result_name}', fg='blue'))
|
|
145
|
+
|
|
146
|
+
return count_result
|
pseek-1.0.2/pseek/searcher.py
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
import click
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from concurrent.futures import ThreadPoolExecutor
|
|
5
|
-
|
|
6
|
-
from .utils import highlight_matches, safe_is_file
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def search_in_names(base_path, query, case_sensitive, regex, include, exclude, whole_word, max_size, min_size,
|
|
10
|
-
full_path, ext=tuple(), is_file=True):
|
|
11
|
-
"""Search for file and folder names"""
|
|
12
|
-
base_path = Path(base_path)
|
|
13
|
-
matches = []
|
|
14
|
-
|
|
15
|
-
if not regex:
|
|
16
|
-
query = re.escape(query) # If regex is disabled, convert query to plain text
|
|
17
|
-
|
|
18
|
-
if whole_word:
|
|
19
|
-
query = rf'\b{query}\b' # Ensure search for whole words
|
|
20
|
-
|
|
21
|
-
flags = 0 if case_sensitive else re.IGNORECASE # Adjust case sensitivity
|
|
22
|
-
|
|
23
|
-
# Convert include and exclude paths to `Path`
|
|
24
|
-
include_paths = {Path(p).resolve() for p in include}
|
|
25
|
-
exclude_paths = {Path(p).resolve() for p in exclude}
|
|
26
|
-
|
|
27
|
-
for p in base_path.rglob('*'):
|
|
28
|
-
p_resolved = p.resolve() # Actual file or folder path
|
|
29
|
-
try:
|
|
30
|
-
p_size_mb = p_resolved.stat().st_size / 1_048_576 # Convert to MB
|
|
31
|
-
except OSError:
|
|
32
|
-
continue # if path cannot be accessible
|
|
33
|
-
|
|
34
|
-
if (
|
|
35
|
-
# If include is given, at least one of them must be present in the name
|
|
36
|
-
(include and not any(p_resolved.is_relative_to(inc) for inc in include_paths))
|
|
37
|
-
# If exclude is given, none should be in the name
|
|
38
|
-
or (exclude and any(p_resolved.is_relative_to(exc) for exc in exclude_paths))
|
|
39
|
-
# Prevent unnecessary continuation in case of type mismatch
|
|
40
|
-
or (is_file and ext and p.suffix[1:] not in ext)
|
|
41
|
-
# Check file or directory size
|
|
42
|
-
or (max_size and p_size_mb > max_size)
|
|
43
|
-
or (min_size and p_size_mb < min_size)
|
|
44
|
-
):
|
|
45
|
-
continue
|
|
46
|
-
|
|
47
|
-
if re.search(query, p.name, flags) and ((is_file and p.is_file()) or (not is_file and p.is_dir())):
|
|
48
|
-
# Specify the found part
|
|
49
|
-
highlighted_name = re.sub(query, lambda m: click.style(m.group(), fg='green'), p.name, flags=flags)
|
|
50
|
-
p_parent = p_resolved.parent if full_path else p.parent # Get full path if full_path is True
|
|
51
|
-
matches.append(f'{p_parent}\\{highlighted_name}')
|
|
52
|
-
|
|
53
|
-
return matches
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def search_in_file_contents(base_path, query, case_sensitive, ext, regex, include, exclude, whole_word,
|
|
57
|
-
max_size, min_size, full_path):
|
|
58
|
-
"""Search inside file contents"""
|
|
59
|
-
base_path = Path(base_path)
|
|
60
|
-
matches = []
|
|
61
|
-
|
|
62
|
-
if not regex:
|
|
63
|
-
query = re.escape(query)
|
|
64
|
-
|
|
65
|
-
if whole_word:
|
|
66
|
-
query = rf'\b{query}\b'
|
|
67
|
-
|
|
68
|
-
flags = 0 if case_sensitive else re.IGNORECASE
|
|
69
|
-
|
|
70
|
-
include_paths = {Path(p).resolve() for p in include}
|
|
71
|
-
exclude_paths = {Path(p).resolve() for p in exclude}
|
|
72
|
-
|
|
73
|
-
def process_file(file_path):
|
|
74
|
-
"""Processes a single file and searches for the query inside its content."""
|
|
75
|
-
p_resolved = file_path.resolve()
|
|
76
|
-
p_size_mb = p_resolved.stat().st_size / 1_048_576
|
|
77
|
-
|
|
78
|
-
if (
|
|
79
|
-
# Filter by include/exclude directories
|
|
80
|
-
(include and not any(p_resolved.is_relative_to(inc) for inc in include_paths))
|
|
81
|
-
or (exclude and any(p_resolved.is_relative_to(exc) for exc in exclude_paths))
|
|
82
|
-
# Filter by extension
|
|
83
|
-
or (ext and p_resolved.suffix[1:] not in ext)
|
|
84
|
-
# Filter by file size
|
|
85
|
-
or (max_size and p_size_mb > max_size)
|
|
86
|
-
or (min_size and p_size_mb < min_size)
|
|
87
|
-
):
|
|
88
|
-
return
|
|
89
|
-
|
|
90
|
-
try:
|
|
91
|
-
for num, line in enumerate(p_resolved.read_text(encoding='utf-8', errors='ignore').splitlines(), 1):
|
|
92
|
-
line = line.strip()
|
|
93
|
-
|
|
94
|
-
if re.search(query, line, flags):
|
|
95
|
-
highlighted_snippet, count_query = highlight_matches(line, query, case_sensitive, regex, whole_word)
|
|
96
|
-
file_path = p_resolved if full_path else file_path
|
|
97
|
-
matches.append(
|
|
98
|
-
click.style(file_path, fg='cyan')
|
|
99
|
-
+ click.style(f' (Line {num}) (Repeated {count_query} time(s)): ', fg='magenta')
|
|
100
|
-
+ highlighted_snippet
|
|
101
|
-
)
|
|
102
|
-
except Exception:
|
|
103
|
-
return # Ignore unreadable files
|
|
104
|
-
|
|
105
|
-
# Use multi-threading for faster processing
|
|
106
|
-
with ThreadPoolExecutor() as executor:
|
|
107
|
-
executor.map(process_file, (file for file in base_path.rglob('*') if safe_is_file(file)))
|
|
108
|
-
|
|
109
|
-
return matches
|
pseek-1.0.2/pseek/utils.py
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
import click
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
def highlight_matches(line, query, case_sensitive, regex, whole_word, context=20):
|
|
6
|
-
"""Highlight matches and truncate long lines with proper handling"""
|
|
7
|
-
|
|
8
|
-
flags = 0 if case_sensitive else re.IGNORECASE
|
|
9
|
-
|
|
10
|
-
if not regex and not whole_word:
|
|
11
|
-
query = re.escape(query) # If regex is disabled, use escape to search for regular text
|
|
12
|
-
|
|
13
|
-
# Find all matches
|
|
14
|
-
matches = list(re.finditer(query, line, flags))
|
|
15
|
-
|
|
16
|
-
if not matches:
|
|
17
|
-
return None # If there is no match, return None
|
|
18
|
-
|
|
19
|
-
start_positions = [max(0, m.start() - context) for m in matches] # 20 characters before
|
|
20
|
-
end_positions = [min(len(line), m.end() + context) for m in matches] # 20 characters after
|
|
21
|
-
|
|
22
|
-
# Combine sections close together to avoid repetition
|
|
23
|
-
merged_snippets = []
|
|
24
|
-
current_start, current_end = start_positions[0], end_positions[0]
|
|
25
|
-
|
|
26
|
-
for i in range(1, len(matches)):
|
|
27
|
-
if start_positions[i] <= current_end: # If the next section is close, combine them
|
|
28
|
-
current_end = max(current_end, end_positions[i])
|
|
29
|
-
else:
|
|
30
|
-
merged_snippets.append((current_start, current_end))
|
|
31
|
-
current_start, current_end = start_positions[i], end_positions[i]
|
|
32
|
-
|
|
33
|
-
merged_snippets.append((current_start, current_end)) # Add the last section
|
|
34
|
-
|
|
35
|
-
result = []
|
|
36
|
-
for start, end in merged_snippets:
|
|
37
|
-
snippet = line[start:end]
|
|
38
|
-
snippet = re.sub(query, lambda m: click.style(m.group(), fg='green'), snippet, flags=flags)
|
|
39
|
-
result.append(snippet)
|
|
40
|
-
|
|
41
|
-
final_output = ' ... '.join(result) # If there are multiple results, put `...` between them
|
|
42
|
-
|
|
43
|
-
# Add `...` at the beginning and end if needed
|
|
44
|
-
if merged_snippets[0][0] > 0:
|
|
45
|
-
final_output = '...' + final_output
|
|
46
|
-
if merged_snippets[-1][1] < len(line):
|
|
47
|
-
final_output += '...'
|
|
48
|
-
|
|
49
|
-
return final_output, len(matches) # Count the number of repetitions
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def safe_is_file(file):
|
|
53
|
-
"""Check if a file is accessible and return True/False."""
|
|
54
|
-
try:
|
|
55
|
-
return file.is_file()
|
|
56
|
-
except OSError:
|
|
57
|
-
return False # Ignore files that cannot be accessed
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def display_results(results, title, result_name):
|
|
61
|
-
"""Display search results"""
|
|
62
|
-
if results:
|
|
63
|
-
click.echo(click.style(f'\n{title}:\n', fg='yellow'))
|
|
64
|
-
for result in results:
|
|
65
|
-
click.echo(result)
|
|
66
|
-
click.echo(click.style(f'\n{len(results)} result(s) found for {result_name}', fg='blue'))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|