blob: 12513594b76f8fd71ba14e11d9ce8b825244ccd4 [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
Cheryl Sabellad60f6582019-03-23 07:33:42 -040043def 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
David Scherer7aced172000-08-15 01:13:23 +000064class GrepDialog(SearchDialogBase):
csabella65474b92017-06-27 02:41:08 -040065 "Dialog for searching multiple files."
David Scherer7aced172000-08-15 01:13:23 +000066
67 title = "Find in Files Dialog"
68 icon = "Grep"
69 needwrapbutton = 0
70
71 def __init__(self, root, engine, flist):
csabella65474b92017-06-27 02:41:08 -040072 """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:
Cheryl Sabella0bb5e752019-03-16 19:29:33 -040078 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.
csabella65474b92017-06-27 02:41:08 -040084 """
Cheryl Sabella0bb5e752019-03-16 19:29:33 -040085 super().__init__(root, engine)
David Scherer7aced172000-08-15 01:13:23 +000086 self.flist = flist
87 self.globvar = StringVar(root)
88 self.recvar = BooleanVar(root)
89
Kurt B. Kaiseref58adf2003-06-07 03:21:17 +000090 def open(self, text, searchphrase, io=None):
Cheryl Sabella0bb5e752019-03-16 19:29:33 -040091 """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 """
Kurt B. Kaiseref58adf2003-06-07 03:21:17 +0000101 SearchDialogBase.open(self, text, searchphrase)
David Scherer7aced172000-08-15 01:13:23 +0000102 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):
csabella65474b92017-06-27 02:41:08 -0400113 "Create base entry widgets and add widget for search path."
David Scherer7aced172000-08-15 01:13:23 +0000114 SearchDialogBase.create_entries(self)
Terry Jan Reedy5283c4e2014-07-13 17:27:26 -0400115 self.globent = self.make_entry("In files:", self.globvar)[0]
David Scherer7aced172000-08-15 01:13:23 +0000116
117 def create_other_buttons(self):
csabella65474b92017-06-27 02:41:08 -0400118 "Add check button to recurse down subdirectories."
Terry Jan Reedy6f7b0f52016-07-10 20:21:31 -0400119 btn = Checkbutton(
120 self.make_frame()[0], variable=self.recvar,
David Scherer7aced172000-08-15 01:13:23 +0000121 text="Recurse down subdirectories")
122 btn.pack(side="top", fill="both")
David Scherer7aced172000-08-15 01:13:23 +0000123
124 def create_command_buttons(self):
Cheryl Sabella0bb5e752019-03-16 19:29:33 -0400125 "Create base command buttons and add button for Search Files."
David Scherer7aced172000-08-15 01:13:23 +0000126 SearchDialogBase.create_command_buttons(self)
Cheryl Sabella0bb5e752019-03-16 19:29:33 -0400127 self.make_button("Search Files", self.default_command, isdef=True)
David Scherer7aced172000-08-15 01:13:23 +0000128
129 def default_command(self, event=None):
csabella65474b92017-06-27 02:41:08 -0400130 """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 """
David Scherer7aced172000-08-15 01:13:23 +0000137 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
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -0400144 from idlelib.outwin import OutputWindow # leave here!
David Scherer7aced172000-08-15 01:13:23 +0000145 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):
csabella65474b92017-06-27 02:41:08 -0400153 """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).
Cheryl Sabella0bb5e752019-03-16 19:29:33 -0400159
160 Args:
161 prog: The compiled, cooked search pattern.
162 path: String containing the search path.
csabella65474b92017-06-27 02:41:08 -0400163 """
Cheryl Sabellad60f6582019-03-23 07:33:42 -0400164 folder, filepat = os.path.split(path)
165 if not folder:
166 folder = os.curdir
167 filelist = sorted(findfiles(folder, filepat, self.recvar.get()))
David Scherer7aced172000-08-15 01:13:23 +0000168 self.close()
169 pat = self.engine.getpat()
csabella65474b92017-06-27 02:41:08 -0400170 print(f"Searching {pat!r} in {path} ...")
David Scherer7aced172000-08-15 01:13:23 +0000171 hits = 0
Terry Jan Reedy47623822014-06-10 02:49:35 -0400172 try:
Cheryl Sabellad60f6582019-03-23 07:33:42 -0400173 for fn in filelist:
Terry Jan Reedy47623822014-06-10 02:49:35 -0400174 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):
csabella65474b92017-06-27 02:41:08 -0400180 sys.stdout.write(f"{fn}: {lineno}: {line}\n")
Terry Jan Reedy47623822014-06-10 02:49:35 -0400181 hits += 1
182 except OSError as msg:
183 print(msg)
csabella65474b92017-06-27 02:41:08 -0400184 print(f"Hits found: {hits}\n(Hint: right-click to open locations.)"
185 if hits else "No hits.")
Terry Jan Reedy47623822014-06-10 02:49:35 -0400186 except AttributeError:
187 # Tk window has been closed, OutputWindow.text = None,
188 # so in OW.write, OW.text.insert fails.
189 pass
David Scherer7aced172000-08-15 01:13:23 +0000190
Terry Jan Reedy47623822014-06-10 02:49:35 -0400191
Terry Jan Reedycd567362014-10-17 01:31:35 -0400192def _grep_dialog(parent): # htest #
Terry Jan Reedy6f7b0f52016-07-10 20:21:31 -0400193 from tkinter import Toplevel, Text, SEL, END
Terry Jan Reedyaff0ada2019-01-02 22:04:06 -0500194 from tkinter.ttk import Frame, Button
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -0400195 from idlelib.pyshell import PyShellFileList
Terry Jan Reedyaff0ada2019-01-02 22:04:06 -0500196
Terry Jan Reedyb60adc52016-06-21 18:41:38 -0400197 top = Toplevel(parent)
198 top.title("Test GrepDialog")
Terry Jan Reedya7480322016-07-10 17:28:10 -0400199 x, y = map(int, parent.geometry().split('+')[1:])
csabella65474b92017-06-27 02:41:08 -0400200 top.geometry(f"+{x}+{y + 175}")
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -0400201
Terry Jan Reedyb60adc52016-06-21 18:41:38 -0400202 flist = PyShellFileList(top)
Terry Jan Reedyaff0ada2019-01-02 22:04:06 -0500203 frame = Frame(top)
204 frame.pack()
205 text = Text(frame, height=5)
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -0400206 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
Terry Jan Reedyaff0ada2019-01-02 22:04:06 -0500213 button = Button(frame, text="Show GrepDialog", command=show_grep_dialog)
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -0400214 button.pack()
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -0400215
Terry Jan Reedyde3beb22013-06-22 18:26:51 -0400216if __name__ == "__main__":
Terry Jan Reedyee5ef302018-06-15 18:20:55 -0400217 from unittest import main
218 main('idlelib.idle_test.test_grep', verbosity=2, exit=False)
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -0400219
220 from idlelib.idle_test.htest import run
221 run(_grep_dialog)