pseek 2.0.0__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-2.0.0 → pseek-2.1.0}/PKG-INFO +2 -2
- {pseek-2.0.0 → 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-2.0.0 → pseek-2.1.0}/pseek.egg-info/PKG-INFO +2 -2
- {pseek-2.0.0 → pseek-2.1.0}/setup.py +1 -1
- pseek-2.0.0/pseek/cli.py +0 -48
- pseek-2.0.0/pseek/searcher.py +0 -146
- {pseek-2.0.0 → pseek-2.1.0}/LICENSE +0 -0
- {pseek-2.0.0 → pseek-2.1.0}/pseek/__init__.py +0 -0
- {pseek-2.0.0 → pseek-2.1.0}/pseek.egg-info/SOURCES.txt +0 -0
- {pseek-2.0.0 → pseek-2.1.0}/pseek.egg-info/dependency_links.txt +0 -0
- {pseek-2.0.0 → pseek-2.1.0}/pseek.egg-info/entry_points.txt +0 -0
- {pseek-2.0.0 → pseek-2.1.0}/pseek.egg-info/requires.txt +0 -0
- {pseek-2.0.0 → pseek-2.1.0}/pseek.egg-info/top_level.txt +0 -0
- {pseek-2.0.0 → pseek-2.1.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: pseek
|
|
3
|
-
Version: 2.
|
|
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: 2.
|
|
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-2.0.0/pseek/cli.py
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
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), default='.',
|
|
8
|
-
show_default=True, help='Base directory to search in.')
|
|
9
|
-
@click.option('-f', '--file', is_flag=True, help='Search only in file names.')
|
|
10
|
-
@click.option('-d', '--directory', is_flag=True, help='Search only in directory names.')
|
|
11
|
-
@click.option('-c', '--content', is_flag=True, help='Search inside file contents.')
|
|
12
|
-
@click.option('-C', '--case-sensitive', is_flag=True, help='Make the search case-sensitive.')
|
|
13
|
-
@click.option('--ext', multiple=True, type=click.STRING,
|
|
14
|
-
help='Filter results by file extension. Example: --ext py --ext js')
|
|
15
|
-
@click.option('--regex', is_flag=True, help='Use regular expression for searching.')
|
|
16
|
-
@click.option('-i', '--include', type=click.Path(exists=True, file_okay=True, dir_okay=True),
|
|
17
|
-
multiple=True, help='Directories or files that contain search results.')
|
|
18
|
-
@click.option('-e', '--exclude', type=click.Path(exists=True, file_okay=True, dir_okay=True),
|
|
19
|
-
multiple=True, help='Directories or files not included in search results.')
|
|
20
|
-
@click.option('--word', is_flag=True, help='Search for results that match the whole word.')
|
|
21
|
-
@click.option('--max-size', type=click.FLOAT, help='Maximum size directory or file can have (MB)')
|
|
22
|
-
@click.option('--min-size', type=click.FLOAT, help='Minimum size directory or file can have (MB)')
|
|
23
|
-
@click.option('--full-path', is_flag=True, help='Display complete paths of files and directories.')
|
|
24
|
-
def search(query, path, file, directory, content, case_sensitive, ext, regex, include, exclude, word,
|
|
25
|
-
max_size, min_size, full_path):
|
|
26
|
-
"""Search for files, directories, and content based on the query."""
|
|
27
|
-
|
|
28
|
-
# Enable all search types if none are explicitly selected
|
|
29
|
-
if not any([file, directory, content]):
|
|
30
|
-
file = directory = content = True
|
|
31
|
-
|
|
32
|
-
count_results = 0
|
|
33
|
-
search_class = Search(path, query, case_sensitive, ext, regex, include, exclude, word, max_size, min_size,
|
|
34
|
-
full_path)
|
|
35
|
-
|
|
36
|
-
if file:
|
|
37
|
-
count_results += search_class.search('file').echo('Files', 'file')
|
|
38
|
-
if directory and not ext:
|
|
39
|
-
count_results += search_class.search('directory').echo('Directories', 'directory')
|
|
40
|
-
if content:
|
|
41
|
-
count_results += search_class.search('content').echo('Contents', 'content')
|
|
42
|
-
|
|
43
|
-
message = f'\nTotal results: {count_results}' if count_results else 'No results found'
|
|
44
|
-
click.echo(click.style(message, fg='red')) # Display total of results
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if __name__ == "__main__":
|
|
48
|
-
search()
|
pseek-2.0.0/pseek/searcher.py
DELETED
|
@@ -1,146 +0,0 @@
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|