blob: 6068d7e4dfce0acb88a87c24b50bd9d05b80c8c6 [file] [log] [blame]
csabella65474b92017-06-27 02:41:08 -04001"""Grep dialog for Find in Files functionality.
2
3 Inherits from SearchDialogBase for GUI and uses searchengine
4 to prepare search pattern.
5"""
David Scherer7aced172000-08-15 01:13:23 +00006import fnmatch
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -04007import os
David Scherer7aced172000-08-15 01:13:23 +00008import sys
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -04009
Terry Jan Reedy6f7b0f52016-07-10 20:21:31 -040010from tkinter import StringVar, BooleanVar
Terry Jan Reedyaff0ada2019-01-02 22:04:06 -050011from tkinter.ttk import Checkbutton # Frame imported in ...Base
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -040012
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -040013from idlelib.searchbase import SearchDialogBase
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -040014from idlelib import searchengine
15
16# Importing OutputWindow here fails due to import loop
Cheryl Sabella0bb5e752019-03-16 19:29:33 -040017# EditorWindow -> GrepDialog -> OutputWindow -> EditorWindow
David Scherer7aced172000-08-15 01:13:23 +000018
csabella65474b92017-06-27 02:41:08 -040019
David Scherer7aced172000-08-15 01:13:23 +000020def grep(text, io=None, flist=None):
Cheryl Sabella0bb5e752019-03-16 19:29:33 -040021 """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.
csabella65474b92017-06-27 02:41:08 -040027
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 """
David Scherer7aced172000-08-15 01:13:23 +000034 root = text._root()
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -040035 engine = searchengine.get(root)
David Scherer7aced172000-08-15 01:13:23 +000036 if not hasattr(engine, "_grepdialog"):
37 engine._grepdialog = GrepDialog(root, engine, flist)
38 dialog = engine._grepdialog
Kurt B. Kaiseref58adf2003-06-07 03:21:17 +000039 searchphrase = text.get("sel.first", "sel.last")
40 dialog.open(text, searchphrase, io)
David Scherer7aced172000-08-15 01:13:23 +000041
csabella65474b92017-06-27 02:41:08 -040042
David Scherer7aced172000-08-15 01:13:23 +000043class GrepDialog(SearchDialogBase):
csabella65474b92017-06-27 02:41:08 -040044 "Dialog for searching multiple files."
David Scherer7aced172000-08-15 01:13:23 +000045
46 title = "Find in Files Dialog"
47 icon = "Grep"
48 needwrapbutton = 0
49
50 def __init__(self, root, engine, flist):
csabella65474b92017-06-27 02:41:08 -040051 """Create search dialog for searching for a phrase in the file system.
52
53 Uses SearchDialogBase as the basis for the GUI and a
54 searchengine instance to prepare the search.
55
56 Attributes:
Cheryl Sabella0bb5e752019-03-16 19:29:33 -040057 flist: filelist.Filelist instance for OutputWindow parent.
58 globvar: String value of Entry widget for path to search.
59 globent: Entry widget for globvar. Created in
60 create_entries().
61 recvar: Boolean value of Checkbutton widget for
62 traversing through subdirectories.
csabella65474b92017-06-27 02:41:08 -040063 """
Cheryl Sabella0bb5e752019-03-16 19:29:33 -040064 super().__init__(root, engine)
David Scherer7aced172000-08-15 01:13:23 +000065 self.flist = flist
66 self.globvar = StringVar(root)
67 self.recvar = BooleanVar(root)
68
Kurt B. Kaiseref58adf2003-06-07 03:21:17 +000069 def open(self, text, searchphrase, io=None):
Cheryl Sabella0bb5e752019-03-16 19:29:33 -040070 """Make dialog visible on top of others and ready to use.
71
72 Extend the SearchDialogBase open() to set the initial value
73 for globvar.
74
75 Args:
76 text: Multicall object containing the text information.
77 searchphrase: String phrase to search.
78 io: iomenu.IOBinding instance containing file path.
79 """
Kurt B. Kaiseref58adf2003-06-07 03:21:17 +000080 SearchDialogBase.open(self, text, searchphrase)
David Scherer7aced172000-08-15 01:13:23 +000081 if io:
82 path = io.filename or ""
83 else:
84 path = ""
85 dir, base = os.path.split(path)
86 head, tail = os.path.splitext(base)
87 if not tail:
88 tail = ".py"
89 self.globvar.set(os.path.join(dir, "*" + tail))
90
91 def create_entries(self):
csabella65474b92017-06-27 02:41:08 -040092 "Create base entry widgets and add widget for search path."
David Scherer7aced172000-08-15 01:13:23 +000093 SearchDialogBase.create_entries(self)
Terry Jan Reedy5283c4e2014-07-13 17:27:26 -040094 self.globent = self.make_entry("In files:", self.globvar)[0]
David Scherer7aced172000-08-15 01:13:23 +000095
96 def create_other_buttons(self):
csabella65474b92017-06-27 02:41:08 -040097 "Add check button to recurse down subdirectories."
Terry Jan Reedy6f7b0f52016-07-10 20:21:31 -040098 btn = Checkbutton(
99 self.make_frame()[0], variable=self.recvar,
David Scherer7aced172000-08-15 01:13:23 +0000100 text="Recurse down subdirectories")
101 btn.pack(side="top", fill="both")
David Scherer7aced172000-08-15 01:13:23 +0000102
103 def create_command_buttons(self):
Cheryl Sabella0bb5e752019-03-16 19:29:33 -0400104 "Create base command buttons and add button for Search Files."
David Scherer7aced172000-08-15 01:13:23 +0000105 SearchDialogBase.create_command_buttons(self)
Cheryl Sabella0bb5e752019-03-16 19:29:33 -0400106 self.make_button("Search Files", self.default_command, isdef=True)
David Scherer7aced172000-08-15 01:13:23 +0000107
108 def default_command(self, event=None):
csabella65474b92017-06-27 02:41:08 -0400109 """Grep for search pattern in file path. The default command is bound
110 to <Return>.
111
112 If entry values are populated, set OutputWindow as stdout
113 and perform search. The search dialog is closed automatically
114 when the search begins.
115 """
David Scherer7aced172000-08-15 01:13:23 +0000116 prog = self.engine.getprog()
117 if not prog:
118 return
119 path = self.globvar.get()
120 if not path:
121 self.top.bell()
122 return
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -0400123 from idlelib.outwin import OutputWindow # leave here!
David Scherer7aced172000-08-15 01:13:23 +0000124 save = sys.stdout
125 try:
126 sys.stdout = OutputWindow(self.flist)
127 self.grep_it(prog, path)
128 finally:
129 sys.stdout = save
130
131 def grep_it(self, prog, path):
csabella65474b92017-06-27 02:41:08 -0400132 """Search for prog within the lines of the files in path.
133
134 For the each file in the path directory, open the file and
135 search each line for the matching pattern. If the pattern is
136 found, write the file and line information to stdout (which
137 is an OutputWindow).
Cheryl Sabella0bb5e752019-03-16 19:29:33 -0400138
139 Args:
140 prog: The compiled, cooked search pattern.
141 path: String containing the search path.
csabella65474b92017-06-27 02:41:08 -0400142 """
David Scherer7aced172000-08-15 01:13:23 +0000143 dir, base = os.path.split(path)
144 list = self.findfiles(dir, base, self.recvar.get())
145 list.sort()
146 self.close()
147 pat = self.engine.getpat()
csabella65474b92017-06-27 02:41:08 -0400148 print(f"Searching {pat!r} in {path} ...")
David Scherer7aced172000-08-15 01:13:23 +0000149 hits = 0
Terry Jan Reedy47623822014-06-10 02:49:35 -0400150 try:
151 for fn in list:
152 try:
153 with open(fn, errors='replace') as f:
154 for lineno, line in enumerate(f, 1):
155 if line[-1:] == '\n':
156 line = line[:-1]
157 if prog.search(line):
csabella65474b92017-06-27 02:41:08 -0400158 sys.stdout.write(f"{fn}: {lineno}: {line}\n")
Terry Jan Reedy47623822014-06-10 02:49:35 -0400159 hits += 1
160 except OSError as msg:
161 print(msg)
csabella65474b92017-06-27 02:41:08 -0400162 print(f"Hits found: {hits}\n(Hint: right-click to open locations.)"
163 if hits else "No hits.")
Terry Jan Reedy47623822014-06-10 02:49:35 -0400164 except AttributeError:
165 # Tk window has been closed, OutputWindow.text = None,
166 # so in OW.write, OW.text.insert fails.
167 pass
David Scherer7aced172000-08-15 01:13:23 +0000168
169 def findfiles(self, dir, base, rec):
csabella65474b92017-06-27 02:41:08 -0400170 """Return list of files in the dir that match the base pattern.
171
Cheryl Sabella0bb5e752019-03-16 19:29:33 -0400172 Use the current directory if dir has no value.
csabella65474b92017-06-27 02:41:08 -0400173 If rec is True, recursively iterate through subdirectories.
Cheryl Sabella0bb5e752019-03-16 19:29:33 -0400174
175 Args:
176 dir: Directory path to search.
177 base: File search pattern.
178 rec: Boolean for recursive search through subdirectories.
csabella65474b92017-06-27 02:41:08 -0400179 """
David Scherer7aced172000-08-15 01:13:23 +0000180 try:
181 names = os.listdir(dir or os.curdir)
Terry Jan Reedyc3111fc2014-02-23 00:37:16 -0500182 except OSError as msg:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000183 print(msg)
David Scherer7aced172000-08-15 01:13:23 +0000184 return []
185 list = []
186 subdirs = []
187 for name in names:
188 fn = os.path.join(dir, name)
189 if os.path.isdir(fn):
190 subdirs.append(fn)
191 else:
192 if fnmatch.fnmatch(name, base):
193 list.append(fn)
194 if rec:
195 for subdir in subdirs:
196 list.extend(self.findfiles(subdir, base, rec))
197 return list
198
Terry Jan Reedy47623822014-06-10 02:49:35 -0400199
Terry Jan Reedycd567362014-10-17 01:31:35 -0400200def _grep_dialog(parent): # htest #
Terry Jan Reedy6f7b0f52016-07-10 20:21:31 -0400201 from tkinter import Toplevel, Text, SEL, END
Terry Jan Reedyaff0ada2019-01-02 22:04:06 -0500202 from tkinter.ttk import Frame, Button
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -0400203 from idlelib.pyshell import PyShellFileList
Terry Jan Reedyaff0ada2019-01-02 22:04:06 -0500204
Terry Jan Reedyb60adc52016-06-21 18:41:38 -0400205 top = Toplevel(parent)
206 top.title("Test GrepDialog")
Terry Jan Reedya7480322016-07-10 17:28:10 -0400207 x, y = map(int, parent.geometry().split('+')[1:])
csabella65474b92017-06-27 02:41:08 -0400208 top.geometry(f"+{x}+{y + 175}")
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -0400209
Terry Jan Reedyb60adc52016-06-21 18:41:38 -0400210 flist = PyShellFileList(top)
Terry Jan Reedyaff0ada2019-01-02 22:04:06 -0500211 frame = Frame(top)
212 frame.pack()
213 text = Text(frame, height=5)
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -0400214 text.pack()
215
216 def show_grep_dialog():
217 text.tag_add(SEL, "1.0", END)
218 grep(text, flist=flist)
219 text.tag_remove(SEL, "1.0", END)
220
Terry Jan Reedyaff0ada2019-01-02 22:04:06 -0500221 button = Button(frame, text="Show GrepDialog", command=show_grep_dialog)
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -0400222 button.pack()
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -0400223
Terry Jan Reedyde3beb22013-06-22 18:26:51 -0400224if __name__ == "__main__":
Terry Jan Reedyee5ef302018-06-15 18:20:55 -0400225 from unittest import main
226 main('idlelib.idle_test.test_grep', verbosity=2, exit=False)
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -0400227
228 from idlelib.idle_test.htest import run
229 run(_grep_dialog)