235 lines
8.8 KiB
Python
235 lines
8.8 KiB
Python
"""
|
||
REPL 模式文件内容搜索工具
|
||
支持正则或纯文本搜索,默认搜索当前目录,支持指定文件后缀白名单、cd切换目录
|
||
新增:工作区 workpath,cd仅改变搜索根目录,输出路径为相对于工作区的相对路径
|
||
"""
|
||
import os
|
||
import re
|
||
import sys
|
||
|
||
|
||
# 硬编码排除目录
|
||
EXCLUDE_DIRS = {
|
||
'.git', '.vs', 'node_modules', '__pycache__', '.idea',
|
||
'miniprogram_npm', '_backup_pre_redesign',
|
||
}
|
||
|
||
# 排除的文件扩展名(黑名单)
|
||
EXCLUDE_EXTS = {
|
||
'.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.bmp',
|
||
'.mp3', '.mp4', '.wav', '.avi',
|
||
'.zip', '.rar', '.7z', '.tar', '.gz',
|
||
'.exe', '.dll', '.so', '.dylib',
|
||
'.pyc', '.pyo', '.class', '.o', '.obj',
|
||
'.db', '.sqlite', '.vsidx',
|
||
}
|
||
|
||
# 默认脚本所在目录
|
||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||
|
||
|
||
def should_skip(dirpath):
|
||
"""判断目录是否应跳过"""
|
||
parts = dirpath.replace('\\', '/').split('/')
|
||
return any(p in EXCLUDE_DIRS for p in parts)
|
||
|
||
|
||
def search(pattern, root, workspace, use_regex=True, max_results=50, include_exts=None):
|
||
"""
|
||
搜索文件内容
|
||
:param pattern: 搜索文本/正则
|
||
:param root: 搜索根目录(cd 会改变此值)
|
||
:param workspace: 工作区目录,输出路径将相对于此目录
|
||
:param use_regex: 是否启用正则模式
|
||
:param max_results: 返回结果上限
|
||
:param include_exts: list[str] 文件后缀白名单,如 [".py",".md"];None代表不限制
|
||
"""
|
||
compiled = None
|
||
if use_regex:
|
||
try:
|
||
compiled = re.compile(pattern, re.IGNORECASE)
|
||
except re.error as e:
|
||
print(f' 正则语法错误: {e}')
|
||
return
|
||
|
||
count = 0
|
||
for dirpath, dirnames, filenames in os.walk(root):
|
||
# 就地修改 dirnames 以跳过排除目录
|
||
dirnames[:] = [d for d in dirnames if d not in EXCLUDE_DIRS]
|
||
|
||
for fname in filenames:
|
||
ext = os.path.splitext(fname)[1].lower()
|
||
|
||
# 后缀白名单逻辑:如果设置了白名单,只处理白名单后缀
|
||
if include_exts is not None:
|
||
if ext not in include_exts:
|
||
continue
|
||
else:
|
||
# 无白名单,走旧黑名单逻辑
|
||
if ext in EXCLUDE_EXTS:
|
||
continue
|
||
|
||
fpath = os.path.join(dirpath, fname)
|
||
# 输出路径:相对于 workspace,而不是 root
|
||
relpath = os.path.relpath(fpath, workspace)
|
||
|
||
try:
|
||
with open(fpath, 'r', encoding='utf-8', errors='ignore') as f:
|
||
for lineno, line in enumerate(f, 1):
|
||
line_stripped = line.rstrip('\n\r')
|
||
hit = False
|
||
highlight_line = line_stripped
|
||
|
||
if use_regex:
|
||
match = compiled.search(line_stripped)
|
||
if match:
|
||
highlight_line = (
|
||
line_stripped[:match.start()]
|
||
+ f'>>> {match.group()} <<<'
|
||
+ line_stripped[match.end():]
|
||
)
|
||
hit = True
|
||
else:
|
||
if pattern.lower() in line_stripped.lower():
|
||
hit = True
|
||
|
||
if hit:
|
||
print(f' {relpath}:{lineno} {highlight_line}')
|
||
count += 1
|
||
if count >= max_results:
|
||
print(f'\n ... 已达上限 {max_results} 条,停止')
|
||
return
|
||
|
||
except (PermissionError, OSError):
|
||
continue
|
||
|
||
if count == 0:
|
||
print(' 未找到匹配内容')
|
||
else:
|
||
print(f'\n 共 {count} 条匹配')
|
||
|
||
|
||
def main():
|
||
print('=== 文件内容搜索工具 ===')
|
||
# 初始化:搜索根目录、工作区都等于脚本目录
|
||
root = SCRIPT_DIR
|
||
workspace = SCRIPT_DIR
|
||
|
||
print(f'搜索根目录(root): {root}')
|
||
print(f'工作区(workspace): {workspace}')
|
||
print(f'排除目录: {EXCLUDE_DIRS}')
|
||
print()
|
||
print('命令:')
|
||
print(' /regex <pattern> 正则搜索')
|
||
print(' /text <text> 纯文本搜索')
|
||
print(' /max <n> 设置结果上限(默认50)')
|
||
print(' /dir <path> 设置搜索根目录(绝对路径)')
|
||
print(' /workpath <path> 设置工作区(支持相对路径);输出路径相对于工作区')
|
||
print(' cd <path> 切换搜索根目录(支持..相对路径,不改变工作区)')
|
||
print(' /ext .py,.md,.txt 设置只搜索的后缀,清空:/ext clear')
|
||
print(' /help 显示帮助')
|
||
print(' /quit 退出')
|
||
print()
|
||
print('直接输入内容默认使用【纯文本】搜索,需要正则请加 /regex 前缀')
|
||
print()
|
||
|
||
use_regex = False
|
||
max_results = 50
|
||
include_exts = None # 文件后缀白名单,None=不限制
|
||
|
||
while True:
|
||
try:
|
||
raw = input('find> ').strip()
|
||
except (EOFError, KeyboardInterrupt):
|
||
print('\n再见')
|
||
break
|
||
|
||
if not raw:
|
||
continue
|
||
|
||
if raw == '/quit':
|
||
print('再见')
|
||
break
|
||
|
||
if raw == '/help':
|
||
print(' /regex <pattern> 正则搜索')
|
||
print(' /text <text> 纯文本搜索')
|
||
print(' /max <n> 设置结果上限')
|
||
print(' /dir <path> 设置搜索根目录(绝对路径)')
|
||
print(' /workpath <path> 设置工作区(支持相对路径),输出路径基于工作区')
|
||
print(' cd <path> 切换搜索根目录,支持 cd .. 、cd subdir;不改变工作区')
|
||
print(' /ext .py,.md,.txt 设置后缀白名单;/ext clear 清除白名单')
|
||
print(' /help 显示帮助')
|
||
print(' /quit 退出')
|
||
print(' 直接输入内容默认纯文本搜索')
|
||
continue
|
||
|
||
if raw.startswith('/regex '):
|
||
pattern = raw[7:].strip()
|
||
if pattern:
|
||
search(pattern, root, workspace, use_regex=True, max_results=max_results, include_exts=include_exts)
|
||
continue
|
||
|
||
if raw.startswith('/text '):
|
||
pattern = raw[6:].strip()
|
||
if pattern:
|
||
search(pattern, root, workspace, use_regex=False, max_results=max_results, include_exts=include_exts)
|
||
continue
|
||
|
||
if raw.startswith('/max '):
|
||
try:
|
||
max_results = int(raw[5:].strip())
|
||
print(f' 结果上限设为 {max_results}')
|
||
except ValueError:
|
||
print(' 请输入数字')
|
||
continue
|
||
|
||
if raw.startswith('/dir '):
|
||
new_dir = raw[5:].strip()
|
||
abs_dir = os.path.abspath(new_dir)
|
||
if os.path.isdir(abs_dir):
|
||
root = abs_dir
|
||
print(f' 搜索根目录切换为: {root}')
|
||
else:
|
||
print(f' 目录不存在: {new_dir}')
|
||
continue
|
||
|
||
# cd:仅修改搜索根目录 root,**不触碰 workspace**
|
||
if raw.startswith('cd '):
|
||
arg = raw[3:].strip()
|
||
new_root = os.path.abspath(os.path.join(root, arg))
|
||
if os.path.isdir(new_root):
|
||
root = new_root
|
||
print(f' cd → {root}')
|
||
else:
|
||
print(f' cd: 目录不存在 {arg}')
|
||
continue
|
||
|
||
# 新增 /workpath:设置工作区,支持相对路径
|
||
if raw.startswith('/workpath '):
|
||
arg = raw[10:].strip()
|
||
new_ws = os.path.abspath(os.path.join(SCRIPT_DIR, arg))
|
||
if os.path.isdir(new_ws):
|
||
workspace = new_ws
|
||
print(f' 工作区已设置: {workspace}')
|
||
else:
|
||
print(f' workpath: 目录不存在 {arg}')
|
||
continue
|
||
|
||
if raw.startswith('/ext '):
|
||
arg = raw[5:].strip().lower()
|
||
if arg == 'clear':
|
||
include_exts = None
|
||
print(' 已清除后缀白名单,搜索全部非排除文件')
|
||
else:
|
||
parts = [p.strip() for p in arg.split(',') if p.strip()]
|
||
include_exts = parts
|
||
print(f' 后缀白名单已设置: {include_exts}')
|
||
continue
|
||
|
||
# 直接输入走纯文本搜索
|
||
search(raw, root, workspace, use_regex=False, max_results=max_results, include_exts=include_exts)
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main() |