pseek 2.1.1__tar.gz → 2.1.2__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.1.1 → pseek-2.1.2}/PKG-INFO +1 -1
- {pseek-2.1.1 → pseek-2.1.2}/pseek/cli.py +1 -1
- {pseek-2.1.1 → pseek-2.1.2}/pseek/searcher.py +64 -53
- {pseek-2.1.1 → pseek-2.1.2}/pseek.egg-info/PKG-INFO +1 -1
- {pseek-2.1.1 → pseek-2.1.2}/setup.py +1 -1
- {pseek-2.1.1 → pseek-2.1.2}/LICENSE +0 -0
- {pseek-2.1.1 → pseek-2.1.2}/README.md +0 -0
- {pseek-2.1.1 → pseek-2.1.2}/pseek/__init__.py +0 -0
- {pseek-2.1.1 → pseek-2.1.2}/pseek.egg-info/SOURCES.txt +0 -0
- {pseek-2.1.1 → pseek-2.1.2}/pseek.egg-info/dependency_links.txt +0 -0
- {pseek-2.1.1 → pseek-2.1.2}/pseek.egg-info/entry_points.txt +0 -0
- {pseek-2.1.1 → pseek-2.1.2}/pseek.egg-info/requires.txt +0 -0
- {pseek-2.1.1 → pseek-2.1.2}/pseek.egg-info/top_level.txt +0 -0
- {pseek-2.1.1 → pseek-2.1.2}/setup.cfg +0 -0
|
@@ -13,7 +13,7 @@ from .searcher import Search
|
|
|
13
13
|
# Additional options
|
|
14
14
|
@click.option('-C', '--case-sensitive', is_flag=True, help='Make the search case-sensitive.')
|
|
15
15
|
@click.option('--regex', is_flag=True, help='Use regular expression for searching.')
|
|
16
|
-
@click.option('--word', is_flag=True, help='Match whole words only.')
|
|
16
|
+
@click.option('-w', '--word', is_flag=True, help='Match whole words only.')
|
|
17
17
|
# Extension filters
|
|
18
18
|
@click.option('--ext', multiple=True, type=click.STRING,
|
|
19
19
|
help='Include files with these extensions. Example: --ext py --ext js')
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import re
|
|
2
2
|
import sys
|
|
3
3
|
import click
|
|
4
|
+
import mmap
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from concurrent.futures import ThreadPoolExecutor
|
|
6
7
|
|
|
@@ -22,21 +23,21 @@ class Search:
|
|
|
22
23
|
def __init__(self, base_path, query, case_sensitive, ext, exclude_ext, regex, include, exclude, whole_word,
|
|
23
24
|
max_size, min_size, full_path):
|
|
24
25
|
"""Initialize search parameters"""
|
|
25
|
-
self.base_path = base_path
|
|
26
|
+
self.base_path = Path(base_path)
|
|
26
27
|
self.query = query
|
|
27
28
|
self.case_sensitive = case_sensitive
|
|
28
|
-
self.ext = ext
|
|
29
|
-
self.exclude_ext = exclude_ext
|
|
29
|
+
self.ext = set(ext)
|
|
30
|
+
self.exclude_ext = set(exclude_ext)
|
|
30
31
|
self.regex = regex
|
|
31
|
-
self.include = include
|
|
32
|
-
self.exclude = exclude
|
|
32
|
+
self.include = {Path(p).resolve() for p in include}
|
|
33
|
+
self.exclude = {Path(p).resolve() for p in exclude}
|
|
33
34
|
self.whole_word = whole_word
|
|
34
35
|
self.max_size = max_size
|
|
35
36
|
self.min_size = min_size
|
|
36
37
|
self.full_path = full_path
|
|
37
38
|
self.result = None
|
|
38
39
|
|
|
39
|
-
def
|
|
40
|
+
def should_skip(self, p_resolved: Path, search_type: str) -> bool:
|
|
40
41
|
"""
|
|
41
42
|
Check whether the file/directory should be skipped based on various filters.
|
|
42
43
|
Returns True if the path should be skipped.
|
|
@@ -47,15 +48,10 @@ class Search:
|
|
|
47
48
|
# If path is inaccessible, skip it.
|
|
48
49
|
return True
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
include_paths = {Path(p).resolve() for p in self.include}
|
|
52
|
-
exclude_paths = {Path(p).resolve() for p in self.exclude}
|
|
51
|
+
file_ext = p_resolved.suffix[1:].lower()
|
|
53
52
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
# Conditions for skipping:
|
|
57
|
-
if (include_paths and not any(p_resolved.is_relative_to(inc) for inc in include_paths)) \
|
|
58
|
-
or (exclude_paths and any(p_resolved.is_relative_to(exc) for exc in exclude_paths)) \
|
|
53
|
+
if (self.include and not any(p_resolved.is_relative_to(inc) for inc in self.include)) \
|
|
54
|
+
or (self.exclude and any(p_resolved.is_relative_to(exc) for exc in self.exclude)) \
|
|
59
55
|
or (self.ext and file_ext not in self.ext) \
|
|
60
56
|
or (self.exclude_ext and file_ext in self.exclude_ext) \
|
|
61
57
|
or (search_type == 'content' and file_ext in EXCLUDED_EXTENSIONS) \
|
|
@@ -69,18 +65,11 @@ class Search:
|
|
|
69
65
|
|
|
70
66
|
def search(self, search_type: str):
|
|
71
67
|
"""Main search function. search_type can be 'file', 'directory' or 'content'"""
|
|
72
|
-
base_path = Path(self.base_path)
|
|
73
68
|
query = self.query
|
|
74
69
|
|
|
75
70
|
# Prepare query: escape if not regex
|
|
76
71
|
if not self.regex:
|
|
77
72
|
query = re.escape(query)
|
|
78
|
-
else:
|
|
79
|
-
try:
|
|
80
|
-
re.compile(query) # Validate regex pattern
|
|
81
|
-
except re.error:
|
|
82
|
-
click.echo(click.style('Invalid regex pattern: ', fg='red') + query)
|
|
83
|
-
sys.exit(1)
|
|
84
73
|
|
|
85
74
|
# Apply whole-word matching only if not using regex (or if desired behavior is defined)
|
|
86
75
|
if self.whole_word and not self.regex:
|
|
@@ -97,13 +86,13 @@ class Search:
|
|
|
97
86
|
|
|
98
87
|
if search_type in ('file', 'directory'):
|
|
99
88
|
matches = []
|
|
100
|
-
for p in base_path.rglob('*'):
|
|
89
|
+
for p in self.base_path.rglob('*'):
|
|
101
90
|
try:
|
|
102
91
|
p_resolved = p.resolve()
|
|
103
92
|
except Exception:
|
|
104
93
|
continue
|
|
105
94
|
# Skip if conditions fail or if name doesn't match the query
|
|
106
|
-
if self.
|
|
95
|
+
if self.should_skip(p_resolved, search_type) or not pattern.search(p.name):
|
|
107
96
|
continue
|
|
108
97
|
|
|
109
98
|
# Highlight matched query in the name
|
|
@@ -111,46 +100,68 @@ class Search:
|
|
|
111
100
|
# Choose parent path based on full_path flag
|
|
112
101
|
p_parent = p_resolved.parent if self.full_path else p.parent
|
|
113
102
|
matches.append(f'{p_parent}\\{highlighted_name}')
|
|
114
|
-
else: #
|
|
103
|
+
else: # content search
|
|
115
104
|
# Use dictionary: key: file path (colored), value: list of line matches
|
|
116
105
|
matches = {}
|
|
106
|
+
# Compile binary version of the pattern
|
|
107
|
+
pattern_bytes = re.compile(pattern.pattern.encode('utf-8'))
|
|
117
108
|
|
|
118
109
|
def process_file(file_path: Path):
|
|
119
110
|
"""Process a single file for content search"""
|
|
120
111
|
try:
|
|
121
|
-
|
|
112
|
+
# Avoid empty files for mmap
|
|
113
|
+
if file_path.stat().st_size == 0:
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
# Open the file in binary read mode
|
|
117
|
+
with open(file_path, 'rb') as f:
|
|
118
|
+
# Memory-map the file for efficient access
|
|
119
|
+
with mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) as mm:
|
|
120
|
+
# Search for the pattern in the entire file as bytes
|
|
121
|
+
if pattern_bytes.search(mm):
|
|
122
|
+
lines = []
|
|
123
|
+
mm.seek(0) # Move the cursor to the beginning of the file
|
|
124
|
+
|
|
125
|
+
# Iterate over each line in the file
|
|
126
|
+
for num, line in enumerate(iter(mm.readline, b''), 1):
|
|
127
|
+
try:
|
|
128
|
+
# Decode the binary line as UTF-8 and strip whitespace
|
|
129
|
+
line_decoded = line.decode('utf-8').strip()
|
|
130
|
+
except UnicodeDecodeError:
|
|
131
|
+
# Skip lines that can't be decoded
|
|
132
|
+
continue
|
|
133
|
+
|
|
134
|
+
# If the pattern matches in the decoded line
|
|
135
|
+
if pattern_list := list(pattern.finditer(line_decoded)):
|
|
136
|
+
# Count how many times the pattern appears in the line
|
|
137
|
+
count = len(pattern_list)
|
|
138
|
+
# Highlight the matching parts in green
|
|
139
|
+
highlighted = pattern.sub(
|
|
140
|
+
lambda m: click.style(m.group(), fg='green'),
|
|
141
|
+
line_decoded
|
|
142
|
+
)
|
|
143
|
+
# Show a note if the pattern repeats 3 or more times
|
|
144
|
+
count_query = f' - Repeated {count} times' if count >= 3 else ''
|
|
145
|
+
# Format the output line with line number and highlighted matches
|
|
146
|
+
lines.append(
|
|
147
|
+
click.style(f'Line {num}{count_query}: ', fg='magenta') + highlighted
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# If any matching lines were found
|
|
151
|
+
if lines:
|
|
152
|
+
# Choose the file path format based on the full_path setting
|
|
153
|
+
file_label = str(file_path.resolve()) if self.full_path else str(file_path)
|
|
154
|
+
# Add the file and its matching lines to the results
|
|
155
|
+
matches[click.style(file_label, fg='cyan')] = lines
|
|
122
156
|
except Exception:
|
|
123
157
|
return
|
|
124
158
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
line_matches = []
|
|
130
|
-
try:
|
|
131
|
-
# Read file line by line
|
|
132
|
-
with open(p_resolved, 'r', encoding='utf-8', errors='ignore') as f:
|
|
133
|
-
for num, line in enumerate(f, 1):
|
|
134
|
-
line = line.strip()
|
|
135
|
-
if pattern.search(line):
|
|
136
|
-
# Count occurrences of query in line
|
|
137
|
-
count = len(list(pattern.finditer(line)))
|
|
138
|
-
highlighted_line = pattern.sub(lambda m: click.style(m.group(), fg='green'), line)
|
|
139
|
-
count_query = f'- Repeated {count} times' if count >= 3 else ''
|
|
140
|
-
line_matches.append(
|
|
141
|
-
click.style(f'Line {num}{count_query}: ', fg='magenta') + highlighted_line
|
|
142
|
-
)
|
|
143
|
-
except Exception:
|
|
144
|
-
return # Skip unreadable files
|
|
145
|
-
|
|
146
|
-
if line_matches:
|
|
147
|
-
file_label = str(p_resolved) if self.full_path else str(file_path)
|
|
148
|
-
# Use colored file path as key
|
|
149
|
-
matches[click.style(file_label, fg='cyan')] = line_matches
|
|
159
|
+
# Filter files before processing
|
|
160
|
+
files_to_process = {
|
|
161
|
+
p for p in self.base_path.rglob('*') if not self.should_skip(p, 'content')
|
|
162
|
+
}
|
|
150
163
|
|
|
151
|
-
|
|
152
|
-
files_to_process = [file for file in base_path.rglob('*')]
|
|
153
|
-
with ThreadPoolExecutor() as executor:
|
|
164
|
+
with ThreadPoolExecutor(max_workers=8) as executor:
|
|
154
165
|
executor.map(process_file, files_to_process)
|
|
155
166
|
|
|
156
167
|
self.result = matches
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|