pseek 1.0.2__tar.gz → 2.1.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.1.0}/PKG-INFO +2 -2
- {pseek-1.0.2 → pseek-2.1.0}/README.md +1 -1
- pseek-2.1.0/pseek/cli.py +73 -0
- pseek-2.1.0/pseek/searcher.py +182 -0
- {pseek-1.0.2 → pseek-2.1.0}/pseek.egg-info/PKG-INFO +2 -2
- {pseek-1.0.2 → pseek-2.1.0}/pseek.egg-info/SOURCES.txt +0 -1
- {pseek-1.0.2 → pseek-2.1.0}/setup.py +1 -1
- pseek-1.0.2/pseek/cli.py +0 -65
- pseek-1.0.2/pseek/searcher.py +0 -109
- pseek-1.0.2/pseek/utils.py +0 -66
- {pseek-1.0.2 → pseek-2.1.0}/LICENSE +0 -0
- {pseek-1.0.2 → pseek-2.1.0}/pseek/__init__.py +0 -0
- {pseek-1.0.2 → pseek-2.1.0}/pseek.egg-info/dependency_links.txt +0 -0
- {pseek-1.0.2 → pseek-2.1.0}/pseek.egg-info/entry_points.txt +0 -0
- {pseek-1.0.2 → pseek-2.1.0}/pseek.egg-info/requires.txt +0 -0
- {pseek-1.0.2 → pseek-2.1.0}/pseek.egg-info/top_level.txt +0 -0
- {pseek-1.0.2 → pseek-2.1.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: pseek
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 2.1.0
|
|
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
|
|
@@ -93,7 +93,7 @@ pseek "error\d+" --regex
|
|
|
93
93
|
| `--file` | Search only in file names |
|
|
94
94
|
| `--directory` | Search only in directory names |
|
|
95
95
|
| `--content` | Search inside file contents |
|
|
96
|
-
| `--ext`
|
|
96
|
+
| `--ext`, `--exclude-ext` | Filter by file extension (e.g., `.txt`, `.log`) |
|
|
97
97
|
| `--case-sensitive` | Make the search case-sensitive |
|
|
98
98
|
| `--regex` | Use regular expression for searching |
|
|
99
99
|
| `--include`, `--exclude` | Limit search results to specific set of directories or files |
|
|
@@ -69,7 +69,7 @@ pseek "error\d+" --regex
|
|
|
69
69
|
| `--file` | Search only in file names |
|
|
70
70
|
| `--directory` | Search only in directory names |
|
|
71
71
|
| `--content` | Search inside file contents |
|
|
72
|
-
| `--ext`
|
|
72
|
+
| `--ext`, `--exclude-ext` | Filter by file extension (e.g., `.txt`, `.log`) |
|
|
73
73
|
| `--case-sensitive` | Make the search case-sensitive |
|
|
74
74
|
| `--regex` | Use regular expression for searching |
|
|
75
75
|
| `--include`, `--exclude` | Limit search results to specific set of directories or files |
|
pseek-2.1.0/pseek/cli.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from .searcher import Search
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@click.command()
|
|
6
|
+
@click.argument('query')
|
|
7
|
+
@click.option('-p', '--path', type=click.Path(exists=True, file_okay=False, dir_okay=True),
|
|
8
|
+
default='.', show_default=True, help='Base directory to search in.')
|
|
9
|
+
# Search type options
|
|
10
|
+
@click.option('-f', '--file', is_flag=True, help='Search only in file names.')
|
|
11
|
+
@click.option('-d', '--directory', is_flag=True, help='Search only in directory names.')
|
|
12
|
+
@click.option('-c', '--content', is_flag=True, help='Search inside file contents.')
|
|
13
|
+
# Additional options
|
|
14
|
+
@click.option('-C', '--case-sensitive', is_flag=True, help='Make the search case-sensitive.')
|
|
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.')
|
|
17
|
+
# Extension filters
|
|
18
|
+
@click.option('--ext', multiple=True, type=click.STRING,
|
|
19
|
+
help='Include files with these extensions. Example: --ext py --ext js')
|
|
20
|
+
@click.option('-E', '--exclude-ext', multiple=True, type=click.STRING,
|
|
21
|
+
help='Exclude files with these extensions. Example: --exclude-ext jpg --exclude-ext exe')
|
|
22
|
+
# Include/Exclude specific paths (files or directories)
|
|
23
|
+
@click.option('-i', '--include', type=click.Path(exists=True, file_okay=True, dir_okay=True),
|
|
24
|
+
multiple=True, help='Directories or files to include in search.')
|
|
25
|
+
@click.option('-e', '--exclude', type=click.Path(exists=True, file_okay=True, dir_okay=True),
|
|
26
|
+
multiple=True, help='Directories or files to exclude from search.')
|
|
27
|
+
# Size filters
|
|
28
|
+
@click.option('--max-size', type=click.FLOAT, help='Maximum file/directory size (in MB).')
|
|
29
|
+
@click.option('--min-size', type=click.FLOAT, help='Minimum file/directory size (in MB).')
|
|
30
|
+
# Output option
|
|
31
|
+
@click.option('--full-path', is_flag=True, help='Display full paths for results.')
|
|
32
|
+
def search(query, path, file, directory, content, case_sensitive, ext, exclude_ext, regex,
|
|
33
|
+
include, exclude, word, max_size, min_size, full_path):
|
|
34
|
+
"""Search for files, directories, and file content based on the query."""
|
|
35
|
+
# If no search type is specified, search in all types.
|
|
36
|
+
if not any((file, directory, content)):
|
|
37
|
+
file = directory = content = True
|
|
38
|
+
|
|
39
|
+
# Initialize the Search class with provided options.
|
|
40
|
+
search_instance = Search(
|
|
41
|
+
base_path=path,
|
|
42
|
+
query=query,
|
|
43
|
+
case_sensitive=case_sensitive,
|
|
44
|
+
ext=ext,
|
|
45
|
+
exclude_ext=exclude_ext,
|
|
46
|
+
regex=regex,
|
|
47
|
+
include=include,
|
|
48
|
+
exclude=exclude,
|
|
49
|
+
whole_word=word,
|
|
50
|
+
max_size=max_size,
|
|
51
|
+
min_size=min_size,
|
|
52
|
+
full_path=full_path
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
total_results = 0
|
|
56
|
+
|
|
57
|
+
# Search for files if requested.
|
|
58
|
+
if file:
|
|
59
|
+
total_results += search_instance.search('file').echo('Files', 'file')
|
|
60
|
+
# Search for directories if requested and extension filters are not active.
|
|
61
|
+
if directory and not (ext or exclude_ext):
|
|
62
|
+
total_results += search_instance.search('directory').echo('Directories', 'directory')
|
|
63
|
+
# Search for content inside files if requested.
|
|
64
|
+
if content:
|
|
65
|
+
total_results += search_instance.search('content').echo('Contents', 'content')
|
|
66
|
+
|
|
67
|
+
# Display final summary message.
|
|
68
|
+
message = f'\nTotal results: {total_results}' if total_results else 'No results found'
|
|
69
|
+
click.echo(click.style(message, fg='red'))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
if __name__ == "__main__":
|
|
73
|
+
search()
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import sys
|
|
3
|
+
import click
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
6
|
+
|
|
7
|
+
# Extensions that are not suitable for content search (binary, media, etc.)
|
|
8
|
+
EXCLUDED_EXTENSIONS = {
|
|
9
|
+
'jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp', 'svg',
|
|
10
|
+
'mp4', 'mov', 'avi', 'mkv', 'webm', 'flv', 'm4v', 'mpg', 'wmv',
|
|
11
|
+
'mp3', 'wav', 'ogg', 'flac', 'aac', 'wma', 'opus',
|
|
12
|
+
'exe', 'dll', 'bin', 'iso', 'img', 'dat', 'dmg', 'class', 'so', 'o', 'obj',
|
|
13
|
+
'zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz',
|
|
14
|
+
'ttf', 'otf', 'woff', 'woff2', 'eot',
|
|
15
|
+
'db', 'sqlite', 'mdf', 'bak', 'log', 'jsonl', 'dat',
|
|
16
|
+
'apk', 'ipa', 'deb', 'rpm', 'pkg', 'appimage', 'jar', 'war',
|
|
17
|
+
'pyc', 'ps1', 'pem', 'pyd', 'whl'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Search:
|
|
22
|
+
def __init__(self, base_path, query, case_sensitive, ext, exclude_ext, regex, include, exclude, whole_word,
|
|
23
|
+
max_size, min_size, full_path):
|
|
24
|
+
"""Initialize search parameters"""
|
|
25
|
+
self.base_path = base_path
|
|
26
|
+
self.query = query
|
|
27
|
+
self.case_sensitive = case_sensitive
|
|
28
|
+
self.ext = ext
|
|
29
|
+
self.exclude_ext = exclude_ext
|
|
30
|
+
self.regex = regex
|
|
31
|
+
self.include = include
|
|
32
|
+
self.exclude = exclude
|
|
33
|
+
self.whole_word = whole_word
|
|
34
|
+
self.max_size = max_size
|
|
35
|
+
self.min_size = min_size
|
|
36
|
+
self.full_path = full_path
|
|
37
|
+
self.result = None
|
|
38
|
+
|
|
39
|
+
def conditions(self, p_resolved: Path, search_type: str) -> bool:
|
|
40
|
+
"""
|
|
41
|
+
Check whether the file/directory should be skipped based on various filters.
|
|
42
|
+
Returns True if the path should be skipped.
|
|
43
|
+
"""
|
|
44
|
+
try:
|
|
45
|
+
p_size_mb = p_resolved.stat().st_size / 1_048_576 # Convert size to MB
|
|
46
|
+
except OSError:
|
|
47
|
+
# If path is inaccessible, skip it.
|
|
48
|
+
return True
|
|
49
|
+
|
|
50
|
+
# Prepare include/exclude sets as resolved Paths
|
|
51
|
+
include_paths = {Path(p).resolve() for p in self.include}
|
|
52
|
+
exclude_paths = {Path(p).resolve() for p in self.exclude}
|
|
53
|
+
|
|
54
|
+
file_ext = p_resolved.suffix[1:]
|
|
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)) \
|
|
59
|
+
or (self.ext and file_ext not in self.ext) \
|
|
60
|
+
or (self.exclude_ext and file_ext in self.exclude_ext) \
|
|
61
|
+
or (search_type == 'content' and file_ext in EXCLUDED_EXTENSIONS) \
|
|
62
|
+
or (self.max_size and p_size_mb > self.max_size) \
|
|
63
|
+
or (self.min_size and p_size_mb < self.min_size) \
|
|
64
|
+
or ((search_type in ('file', 'content')) and not p_resolved.is_file()) \
|
|
65
|
+
or (search_type == 'directory' and not p_resolved.is_dir()):
|
|
66
|
+
return True
|
|
67
|
+
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
def search(self, search_type: str):
|
|
71
|
+
"""Main search function. search_type can be 'file', 'directory' or 'content'"""
|
|
72
|
+
base_path = Path(self.base_path).resolve()
|
|
73
|
+
query = self.query
|
|
74
|
+
|
|
75
|
+
# Prepare query: escape if not regex
|
|
76
|
+
if not self.regex:
|
|
77
|
+
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
|
+
|
|
85
|
+
# Apply whole-word matching only if not using regex (or if desired behavior is defined)
|
|
86
|
+
if self.whole_word and not self.regex:
|
|
87
|
+
query = rf'\b{query}\b'
|
|
88
|
+
|
|
89
|
+
flags = 0 if self.case_sensitive else re.IGNORECASE # Adjust case sensitivity
|
|
90
|
+
|
|
91
|
+
# Precompile the regex pattern for performance
|
|
92
|
+
try:
|
|
93
|
+
pattern = re.compile(query, flags)
|
|
94
|
+
except re.error as e:
|
|
95
|
+
click.echo(click.style(f"Regex compile error: {e}", fg='red'))
|
|
96
|
+
sys.exit(1)
|
|
97
|
+
|
|
98
|
+
if search_type in ('file', 'directory'):
|
|
99
|
+
matches = []
|
|
100
|
+
for p in base_path.rglob('*'):
|
|
101
|
+
try:
|
|
102
|
+
p_resolved = p.resolve()
|
|
103
|
+
except Exception:
|
|
104
|
+
continue
|
|
105
|
+
# Skip if conditions fail or if name doesn't match the query
|
|
106
|
+
if self.conditions(p_resolved, search_type) or not pattern.search(p.name):
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
# Highlight matched query in the name
|
|
110
|
+
highlighted_name = pattern.sub(lambda m: click.style(m.group(), fg='green'), p.name)
|
|
111
|
+
# Choose parent path based on full_path flag
|
|
112
|
+
p_parent = p_resolved.parent if self.full_path else p.parent
|
|
113
|
+
matches.append(f'{p_parent}\\{highlighted_name}')
|
|
114
|
+
else: # For content search
|
|
115
|
+
# Use dictionary: key: file path (colored), value: list of line matches
|
|
116
|
+
matches = {}
|
|
117
|
+
|
|
118
|
+
def process_file(file_path: Path):
|
|
119
|
+
"""Process a single file for content search"""
|
|
120
|
+
try:
|
|
121
|
+
p_resolved = file_path.resolve()
|
|
122
|
+
except Exception:
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
# Skip file if any filter condition is met
|
|
126
|
+
if self.conditions(p_resolved, 'content'):
|
|
127
|
+
return
|
|
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
|
|
150
|
+
|
|
151
|
+
# Create a list of files to process (filter out inaccessible ones early)
|
|
152
|
+
files_to_process = [file for file in base_path.rglob('*')]
|
|
153
|
+
with ThreadPoolExecutor() as executor:
|
|
154
|
+
executor.map(process_file, files_to_process)
|
|
155
|
+
|
|
156
|
+
self.result = matches
|
|
157
|
+
return self
|
|
158
|
+
|
|
159
|
+
def echo(self, title: str, result_name: str) -> int:
|
|
160
|
+
"""
|
|
161
|
+
Display the search results with a title.
|
|
162
|
+
Returns the count of results.
|
|
163
|
+
"""
|
|
164
|
+
count_result = 0
|
|
165
|
+
|
|
166
|
+
if self.result:
|
|
167
|
+
click.echo(click.style(f'\n{title}:\n', fg='yellow'))
|
|
168
|
+
if isinstance(self.result, dict):
|
|
169
|
+
# For content search results
|
|
170
|
+
for key, value in self.result.items():
|
|
171
|
+
click.echo(key)
|
|
172
|
+
click.echo('\n'.join(value) + '\n')
|
|
173
|
+
count_result += len(value)
|
|
174
|
+
else:
|
|
175
|
+
# For file/directory search results
|
|
176
|
+
count_result = len(self.result)
|
|
177
|
+
click.echo('\n'.join(self.result))
|
|
178
|
+
|
|
179
|
+
if count_result >= 3:
|
|
180
|
+
click.echo(click.style(f'\n{count_result} results found for {result_name}', fg='blue'))
|
|
181
|
+
|
|
182
|
+
return count_result
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: pseek
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 2.1.0
|
|
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
|
|
@@ -93,7 +93,7 @@ pseek "error\d+" --regex
|
|
|
93
93
|
| `--file` | Search only in file names |
|
|
94
94
|
| `--directory` | Search only in directory names |
|
|
95
95
|
| `--content` | Search inside file contents |
|
|
96
|
-
| `--ext`
|
|
96
|
+
| `--ext`, `--exclude-ext` | Filter by file extension (e.g., `.txt`, `.log`) |
|
|
97
97
|
| `--case-sensitive` | Make the search case-sensitive |
|
|
98
98
|
| `--regex` | Use regular expression for searching |
|
|
99
99
|
| `--include`, `--exclude` | Limit search results to specific set of directories or files |
|
pseek-1.0.2/pseek/cli.py
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
import click
|
|
3
|
-
|
|
4
|
-
from .utils import display_results
|
|
5
|
-
from .searcher import search_in_names, search_in_file_contents
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
@click.command()
|
|
9
|
-
@click.argument('query')
|
|
10
|
-
@click.option('-p', '--path', type=click.Path(exists=True, file_okay=False, dir_okay=True), default='.',
|
|
11
|
-
show_default=True, help='Base directory to search in.')
|
|
12
|
-
@click.option('-f', '--file', is_flag=True, help='Search only in file names.')
|
|
13
|
-
@click.option('-d', '--directory', is_flag=True, help='Search only in directory names.')
|
|
14
|
-
@click.option('-c', '--content', is_flag=True, help='Search inside file contents.')
|
|
15
|
-
@click.option('-C', '--case-sensitive', is_flag=True, help='Make the search case-sensitive.')
|
|
16
|
-
@click.option('--ext', multiple=True, type=click.STRING,
|
|
17
|
-
help='Filter results by file extension. Example: --ext py --ext js')
|
|
18
|
-
@click.option('--regex', is_flag=True, help='Use regular expression for searching.')
|
|
19
|
-
@click.option('-i', '--include', type=click.Path(exists=True, file_okay=True, dir_okay=True),
|
|
20
|
-
multiple=True, help='Directories or files that contain search results.')
|
|
21
|
-
@click.option('-e', '--exclude', type=click.Path(exists=True, file_okay=True, dir_okay=True),
|
|
22
|
-
multiple=True, help='Directories or files not included in search results.')
|
|
23
|
-
@click.option('--word', is_flag=True, help='Search for results that match the whole word.')
|
|
24
|
-
@click.option('--max-size', type=click.FLOAT, help='Maximum size directory or file can have (MB)')
|
|
25
|
-
@click.option('--min-size', type=click.FLOAT, help='Minimum size directory or file can have (MB)')
|
|
26
|
-
@click.option('--full-path', is_flag=True, help='Display complete paths of files and directories.')
|
|
27
|
-
def search(query, path, file, directory, content, case_sensitive, ext, regex, include, exclude, word,
|
|
28
|
-
max_size, min_size, full_path):
|
|
29
|
-
"""Search for files, directories, and content based on the query."""
|
|
30
|
-
|
|
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
|
-
# Enable all search types if none are explicitly selected
|
|
39
|
-
if not any([file, directory, content]):
|
|
40
|
-
file = directory = content = True
|
|
41
|
-
|
|
42
|
-
results = []
|
|
43
|
-
|
|
44
|
-
if file:
|
|
45
|
-
filename_results = search_in_names(path, query, case_sensitive, regex, include, exclude, word, max_size,
|
|
46
|
-
min_size, full_path, ext)
|
|
47
|
-
display_results(filename_results, 'Files', 'file')
|
|
48
|
-
results.extend(filename_results)
|
|
49
|
-
if directory and not ext:
|
|
50
|
-
dir_results = search_in_names(path, query, case_sensitive, regex, include, exclude, word, max_size, min_size,
|
|
51
|
-
full_path, is_file=False)
|
|
52
|
-
display_results(dir_results, 'Directories', 'directory')
|
|
53
|
-
results.extend(dir_results)
|
|
54
|
-
if content:
|
|
55
|
-
content_results = search_in_file_contents(path, query, case_sensitive, ext, regex, include, exclude, word,
|
|
56
|
-
max_size, min_size, full_path)
|
|
57
|
-
display_results(content_results, 'Contents', 'content')
|
|
58
|
-
results.extend(content_results)
|
|
59
|
-
|
|
60
|
-
message = f'\nTotal results: {len(results)}' if results else 'No results found'
|
|
61
|
-
click.echo(click.style(message, fg='red')) # Display total of results
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if __name__ == "__main__":
|
|
65
|
-
search()
|
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
|