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