1"""Grep dialog for Find in Files functionality. 2 3 Inherits from SearchDialogBase for GUI and uses searchengine 4 to prepare search pattern. 5""" 6import fnmatch 7import os 8import sys 9 10from tkinter import StringVar, BooleanVar 11from tkinter.ttk import Checkbutton # Frame imported in ...Base 12 13from idlelib.searchbase import SearchDialogBase 14from idlelib import searchengine 15 16# Importing OutputWindow here fails due to import loop 17# EditorWindow -> GrepDialog -> OutputWindow -> EditorWindow 18 19 20def grep(text, io=None, flist=None): 21 """Open the Find in Files dialog. 22 23 Module-level function to access the singleton GrepDialog 24 instance and open the dialog. If text is selected, it is 25 used as the search phrase; otherwise, the previous entry 26 is used. 27 28 Args: 29 text: Text widget that contains the selected text for 30 default search phrase. 31 io: iomenu.IOBinding instance with default path to search. 32 flist: filelist.FileList instance for OutputWindow parent. 33 """ 34 root = text._root() 35 engine = searchengine.get(root) 36 if not hasattr(engine, "_grepdialog"): 37 engine._grepdialog = GrepDialog(root, engine, flist) 38 dialog = engine._grepdialog 39 searchphrase = text.get("sel.first", "sel.last") 40 dialog.open(text, searchphrase, io) 41 42 43def walk_error(msg): 44 "Handle os.walk error." 45 print(msg) 46 47 48def findfiles(folder, pattern, recursive): 49 """Generate file names in dir that match pattern. 50 51 Args: 52 folder: Root directory to search. 53 pattern: File pattern to match. 54 recursive: True to include subdirectories. 55 """ 56 for dirpath, _, filenames in os.walk(folder, onerror=walk_error): 57 yield from (os.path.join(dirpath, name) 58 for name in filenames 59 if fnmatch.fnmatch(name, pattern)) 60 if not recursive: 61 break 62 63 64class GrepDialog(SearchDialogBase): 65 "Dialog for searching multiple files." 66 67 title = "Find in Files Dialog" 68 icon = "Grep" 69 needwrapbutton = 0 70 71 def __init__(self, root, engine, flist): 72 """Create search dialog for searching for a phrase in the file system. 73 74 Uses SearchDialogBase as the basis for the GUI and a 75 searchengine instance to prepare the search. 76 77 Attributes: 78 flist: filelist.Filelist instance for OutputWindow parent. 79 globvar: String value of Entry widget for path to search. 80 globent: Entry widget for globvar. Created in 81 create_entries(). 82 recvar: Boolean value of Checkbutton widget for 83 traversing through subdirectories. 84 """ 85 super().__init__(root, engine) 86 self.flist = flist 87 self.globvar = StringVar(root) 88 self.recvar = BooleanVar(root) 89 90 def open(self, text, searchphrase, io=None): 91 """Make dialog visible on top of others and ready to use. 92 93 Extend the SearchDialogBase open() to set the initial value 94 for globvar. 95 96 Args: 97 text: Multicall object containing the text information. 98 searchphrase: String phrase to search. 99 io: iomenu.IOBinding instance containing file path. 100 """ 101 SearchDialogBase.open(self, text, searchphrase) 102 if io: 103 path = io.filename or "" 104 else: 105 path = "" 106 dir, base = os.path.split(path) 107 head, tail = os.path.splitext(base) 108 if not tail: 109 tail = ".py" 110 self.globvar.set(os.path.join(dir, "*" + tail)) 111 112 def create_entries(self): 113 "Create base entry widgets and add widget for search path." 114 SearchDialogBase.create_entries(self) 115 self.globent = self.make_entry("In files:", self.globvar)[0] 116 117 def create_other_buttons(self): 118 "Add check button to recurse down subdirectories." 119 btn = Checkbutton( 120 self.make_frame()[0], variable=self.recvar, 121 text="Recurse down subdirectories") 122 btn.pack(side="top", fill="both") 123 124 def create_command_buttons(self): 125 "Create base command buttons and add button for Search Files." 126 SearchDialogBase.create_command_buttons(self) 127 self.make_button("Search Files", self.default_command, isdef=True) 128 129 def default_command(self, event=None): 130 """Grep for search pattern in file path. The default command is bound 131 to <Return>. 132 133 If entry values are populated, set OutputWindow as stdout 134 and perform search. The search dialog is closed automatically 135 when the search begins. 136 """ 137 prog = self.engine.getprog() 138 if not prog: 139 return 140 path = self.globvar.get() 141 if not path: 142 self.top.bell() 143 return 144 from idlelib.outwin import OutputWindow # leave here! 145 save = sys.stdout 146 try: 147 sys.stdout = OutputWindow(self.flist) 148 self.grep_it(prog, path) 149 finally: 150 sys.stdout = save 151 152 def grep_it(self, prog, path): 153 """Search for prog within the lines of the files in path. 154 155 For the each file in the path directory, open the file and 156 search each line for the matching pattern. If the pattern is 157 found, write the file and line information to stdout (which 158 is an OutputWindow). 159 160 Args: 161 prog: The compiled, cooked search pattern. 162 path: String containing the search path. 163 """ 164 folder, filepat = os.path.split(path) 165 if not folder: 166 folder = os.curdir 167 filelist = sorted(findfiles(folder, filepat, self.recvar.get())) 168 self.close() 169 pat = self.engine.getpat() 170 print(f"Searching {pat!r} in {path} ...") 171 hits = 0 172 try: 173 for fn in filelist: 174 try: 175 with open(fn, errors='replace') as f: 176 for lineno, line in enumerate(f, 1): 177 if line[-1:] == '\n': 178 line = line[:-1] 179 if prog.search(line): 180 sys.stdout.write(f"{fn}: {lineno}: {line}\n") 181 hits += 1 182 except OSError as msg: 183 print(msg) 184 print(f"Hits found: {hits}\n(Hint: right-click to open locations.)" 185 if hits else "No hits.") 186 except AttributeError: 187 # Tk window has been closed, OutputWindow.text = None, 188 # so in OW.write, OW.text.insert fails. 189 pass 190 191 192def _grep_dialog(parent): # htest # 193 from tkinter import Toplevel, Text, SEL, END 194 from tkinter.ttk import Frame, Button 195 from idlelib.pyshell import PyShellFileList 196 197 top = Toplevel(parent) 198 top.title("Test GrepDialog") 199 x, y = map(int, parent.geometry().split('+')[1:]) 200 top.geometry(f"+{x}+{y + 175}") 201 202 flist = PyShellFileList(top) 203 frame = Frame(top) 204 frame.pack() 205 text = Text(frame, height=5) 206 text.pack() 207 208 def show_grep_dialog(): 209 text.tag_add(SEL, "1.0", END) 210 grep(text, flist=flist) 211 text.tag_remove(SEL, "1.0", END) 212 213 button = Button(frame, text="Show GrepDialog", command=show_grep_dialog) 214 button.pack() 215 216if __name__ == "__main__": 217 from unittest import main 218 main('idlelib.idle_test.test_grep', verbosity=2, exit=False) 219 220 from idlelib.idle_test.htest import run 221 run(_grep_dialog) 222