""" 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 正则搜索') print(' /text 纯文本搜索') print(' /max 设置结果上限(默认50)') print(' /dir 设置搜索根目录(绝对路径)') print(' /workpath 设置工作区(支持相对路径);输出路径相对于工作区') print(' cd 切换搜索根目录(支持..相对路径,不改变工作区)') 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 正则搜索') print(' /text 纯文本搜索') print(' /max 设置结果上限') print(' /dir 设置搜索根目录(绝对路径)') print(' /workpath 设置工作区(支持相对路径),输出路径基于工作区') print(' cd 切换搜索根目录,支持 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()