pseek 2.1.1__tar.gz → 2.1.3__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.3}/PKG-INFO +2 -1
- {pseek-2.1.1 → pseek-2.1.3}/README.md +1 -0
- {pseek-2.1.1 → pseek-2.1.3}/pseek/cli.py +5 -3
- {pseek-2.1.1 → pseek-2.1.3}/pseek/searcher.py +73 -55
- {pseek-2.1.1 → pseek-2.1.3}/pseek.egg-info/PKG-INFO +2 -1
- {pseek-2.1.1 → pseek-2.1.3}/setup.py +1 -1
- {pseek-2.1.1 → pseek-2.1.3}/LICENSE +0 -0
- {pseek-2.1.1 → pseek-2.1.3}/pseek/__init__.py +0 -0
- {pseek-2.1.1 → pseek-2.1.3}/pseek.egg-info/SOURCES.txt +0 -0
- {pseek-2.1.1 → pseek-2.1.3}/pseek.egg-info/dependency_links.txt +0 -0
- {pseek-2.1.1 → pseek-2.1.3}/pseek.egg-info/entry_points.txt +0 -0
- {pseek-2.1.1 → pseek-2.1.3}/pseek.egg-info/requires.txt +0 -0
- {pseek-2.1.1 → pseek-2.1.3}/pseek.egg-info/top_level.txt +0 -0
- {pseek-2.1.1 → pseek-2.1.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: pseek
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.3
|
|
4
4
|
Summary: Pseek is a Python library to search files, folders, and text
|
|
5
5
|
Home-page: https://github.com/ArianN8610/pysearch
|
|
6
6
|
Author: Arian
|
|
@@ -100,3 +100,4 @@ pseek "error\d+" --regex
|
|
|
100
100
|
| `--word` | Match the whole word only |
|
|
101
101
|
| `--max-size`, `--min-size` | Specify maximum and minimum sizes for files and directories |
|
|
102
102
|
| `--full-path` | Display full path of files and directories |
|
|
103
|
+
| `--no-content` | Only display files path for content search |
|
|
@@ -76,3 +76,4 @@ pseek "error\d+" --regex
|
|
|
76
76
|
| `--word` | Match the whole word only |
|
|
77
77
|
| `--max-size`, `--min-size` | Specify maximum and minimum sizes for files and directories |
|
|
78
78
|
| `--full-path` | Display full path of files and directories |
|
|
79
|
+
| `--no-content` | Only display files path for content search |
|
|
@@ -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')
|
|
@@ -29,8 +29,9 @@ from .searcher import Search
|
|
|
29
29
|
@click.option('--min-size', type=click.FLOAT, help='Minimum file/directory size (in MB).')
|
|
30
30
|
# Output option
|
|
31
31
|
@click.option('--full-path', is_flag=True, help='Display full paths for results.')
|
|
32
|
+
@click.option('--no-content', is_flag=True, help='Only display files path for content search.')
|
|
32
33
|
def search(query, path, file, directory, content, case_sensitive, ext, exclude_ext, regex,
|
|
33
|
-
include, exclude, word, max_size, min_size, full_path):
|
|
34
|
+
include, exclude, word, max_size, min_size, full_path, no_content):
|
|
34
35
|
"""Search for files, directories, and file content based on the query."""
|
|
35
36
|
# If no search type is specified, search in all types.
|
|
36
37
|
if not any((file, directory, content)):
|
|
@@ -49,7 +50,8 @@ def search(query, path, file, directory, content, case_sensitive, ext, exclude_e
|
|
|
49
50
|
whole_word=word,
|
|
50
51
|
max_size=max_size,
|
|
51
52
|
min_size=min_size,
|
|
52
|
-
full_path=full_path
|
|
53
|
+
full_path=full_path,
|
|
54
|
+
no_content=no_content
|
|
53
55
|
)
|
|
54
56
|
|
|
55
57
|
total_results = 0
|
|
@@ -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
|
|
|
@@ -20,23 +21,24 @@ EXCLUDED_EXTENSIONS = {
|
|
|
20
21
|
|
|
21
22
|
class Search:
|
|
22
23
|
def __init__(self, base_path, query, case_sensitive, ext, exclude_ext, regex, include, exclude, whole_word,
|
|
23
|
-
max_size, min_size, full_path):
|
|
24
|
+
max_size, min_size, full_path, no_content):
|
|
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
|
|
38
|
+
self.no_content = no_content
|
|
37
39
|
self.result = None
|
|
38
40
|
|
|
39
|
-
def
|
|
41
|
+
def should_skip(self, p_resolved: Path, search_type: str) -> bool:
|
|
40
42
|
"""
|
|
41
43
|
Check whether the file/directory should be skipped based on various filters.
|
|
42
44
|
Returns True if the path should be skipped.
|
|
@@ -47,15 +49,10 @@ class Search:
|
|
|
47
49
|
# If path is inaccessible, skip it.
|
|
48
50
|
return True
|
|
49
51
|
|
|
50
|
-
|
|
51
|
-
include_paths = {Path(p).resolve() for p in self.include}
|
|
52
|
-
exclude_paths = {Path(p).resolve() for p in self.exclude}
|
|
52
|
+
file_ext = p_resolved.suffix[1:].lower()
|
|
53
53
|
|
|
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)) \
|
|
54
|
+
if (self.include and not any(p_resolved.is_relative_to(inc) for inc in self.include)) \
|
|
55
|
+
or (self.exclude and any(p_resolved.is_relative_to(exc) for exc in self.exclude)) \
|
|
59
56
|
or (self.ext and file_ext not in self.ext) \
|
|
60
57
|
or (self.exclude_ext and file_ext in self.exclude_ext) \
|
|
61
58
|
or (search_type == 'content' and file_ext in EXCLUDED_EXTENSIONS) \
|
|
@@ -69,18 +66,11 @@ class Search:
|
|
|
69
66
|
|
|
70
67
|
def search(self, search_type: str):
|
|
71
68
|
"""Main search function. search_type can be 'file', 'directory' or 'content'"""
|
|
72
|
-
base_path = Path(self.base_path)
|
|
73
69
|
query = self.query
|
|
74
70
|
|
|
75
71
|
# Prepare query: escape if not regex
|
|
76
72
|
if not self.regex:
|
|
77
73
|
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
74
|
|
|
85
75
|
# Apply whole-word matching only if not using regex (or if desired behavior is defined)
|
|
86
76
|
if self.whole_word and not self.regex:
|
|
@@ -97,13 +87,13 @@ class Search:
|
|
|
97
87
|
|
|
98
88
|
if search_type in ('file', 'directory'):
|
|
99
89
|
matches = []
|
|
100
|
-
for p in base_path.rglob('*'):
|
|
90
|
+
for p in self.base_path.rglob('*'):
|
|
101
91
|
try:
|
|
102
92
|
p_resolved = p.resolve()
|
|
103
93
|
except Exception:
|
|
104
94
|
continue
|
|
105
95
|
# Skip if conditions fail or if name doesn't match the query
|
|
106
|
-
if self.
|
|
96
|
+
if self.should_skip(p_resolved, search_type) or not pattern.search(p.name):
|
|
107
97
|
continue
|
|
108
98
|
|
|
109
99
|
# Highlight matched query in the name
|
|
@@ -111,46 +101,74 @@ class Search:
|
|
|
111
101
|
# Choose parent path based on full_path flag
|
|
112
102
|
p_parent = p_resolved.parent if self.full_path else p.parent
|
|
113
103
|
matches.append(f'{p_parent}\\{highlighted_name}')
|
|
114
|
-
else: #
|
|
104
|
+
else: # content search
|
|
115
105
|
# Use dictionary: key: file path (colored), value: list of line matches
|
|
116
|
-
matches = {}
|
|
106
|
+
matches = {} if not self.no_content else set()
|
|
107
|
+
# Compile binary version of the pattern
|
|
108
|
+
pattern_bytes = re.compile(pattern.pattern.encode('utf-8'))
|
|
117
109
|
|
|
118
110
|
def process_file(file_path: Path):
|
|
119
111
|
"""Process a single file for content search"""
|
|
120
112
|
try:
|
|
121
|
-
|
|
113
|
+
# Avoid empty files for mmap
|
|
114
|
+
if file_path.stat().st_size == 0:
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
# Open the file in binary read mode
|
|
118
|
+
with open(file_path, 'rb') as f:
|
|
119
|
+
# Memory-map the file for efficient access
|
|
120
|
+
with mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) as mm:
|
|
121
|
+
# Search for the pattern in the entire file as bytes
|
|
122
|
+
if pattern_bytes.search(mm):
|
|
123
|
+
# Choose the file path format based on the full_path setting
|
|
124
|
+
file_label = str(file_path.resolve()) if self.full_path else str(file_path)
|
|
125
|
+
|
|
126
|
+
# Avoid searching through the entire file content if the fast-content flag is True
|
|
127
|
+
if self.no_content:
|
|
128
|
+
matches.add(click.style(file_label, fg='cyan'))
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
lines = []
|
|
132
|
+
mm.seek(0) # Move the cursor to the beginning of the file
|
|
133
|
+
|
|
134
|
+
# Iterate over each line in the file
|
|
135
|
+
for num, line in enumerate(iter(mm.readline, b''), 1):
|
|
136
|
+
try:
|
|
137
|
+
# Decode the binary line as UTF-8 and strip whitespace
|
|
138
|
+
line_decoded = line.decode('utf-8').strip()
|
|
139
|
+
except UnicodeDecodeError:
|
|
140
|
+
# Skip lines that can't be decoded
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
# If the pattern matches in the decoded line
|
|
144
|
+
if pattern_list := list(pattern.finditer(line_decoded)):
|
|
145
|
+
# Count how many times the pattern appears in the line
|
|
146
|
+
count = len(pattern_list)
|
|
147
|
+
# Highlight the matching parts in green
|
|
148
|
+
highlighted = pattern.sub(
|
|
149
|
+
lambda m: click.style(m.group(), fg='green'),
|
|
150
|
+
line_decoded
|
|
151
|
+
)
|
|
152
|
+
# Show a note if the pattern repeats 3 or more times
|
|
153
|
+
count_query = f' - Repeated {count} times' if count >= 3 else ''
|
|
154
|
+
# Format the output line with line number and highlighted matches
|
|
155
|
+
lines.append(
|
|
156
|
+
click.style(f'Line {num}{count_query}: ', fg='magenta') + highlighted
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# If any matching lines were found
|
|
160
|
+
if lines:
|
|
161
|
+
# Add the file and its matching lines to the results
|
|
162
|
+
matches[click.style(file_label, fg='cyan')] = lines
|
|
122
163
|
except Exception:
|
|
123
164
|
return
|
|
124
165
|
|
|
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
|
|
166
|
+
# Filter files before processing
|
|
167
|
+
files_to_process = {
|
|
168
|
+
p for p in self.base_path.rglob('*') if not self.should_skip(p, 'content')
|
|
169
|
+
}
|
|
150
170
|
|
|
151
|
-
|
|
152
|
-
files_to_process = [file for file in base_path.rglob('*')]
|
|
153
|
-
with ThreadPoolExecutor() as executor:
|
|
171
|
+
with ThreadPoolExecutor(max_workers=8) as executor:
|
|
154
172
|
executor.map(process_file, files_to_process)
|
|
155
173
|
|
|
156
174
|
self.result = matches
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: pseek
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.3
|
|
4
4
|
Summary: Pseek is a Python library to search files, folders, and text
|
|
5
5
|
Home-page: https://github.com/ArianN8610/pysearch
|
|
6
6
|
Author: Arian
|
|
@@ -100,3 +100,4 @@ pseek "error\d+" --regex
|
|
|
100
100
|
| `--word` | Match the whole word only |
|
|
101
101
|
| `--max-size`, `--min-size` | Specify maximum and minimum sizes for files and directories |
|
|
102
102
|
| `--full-path` | Display full path of files and directories |
|
|
103
|
+
| `--no-content` | Only display files path for content search |
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|