222 lines
7.3 KiB
Python
222 lines
7.3 KiB
Python
|
"""Grep dialog for Find in Files functionality.
|
||
|
|
||
|
Inherits from SearchDialogBase for GUI and uses searchengine
|
||
|
to prepare search pattern.
|
||
|
"""
|
||
|
import fnmatch
|
||
|
import os
|
||
|
import sys
|
||
|
|
||
|
from tkinter import StringVar, BooleanVar
|
||
|
from tkinter.ttk import Checkbutton # Frame imported in ...Base
|
||
|
|
||
|
from idlelib.searchbase import SearchDialogBase
|
||
|
from idlelib import searchengine
|
||
|
|
||
|
# Importing OutputWindow here fails due to import loop
|
||
|
# EditorWindow -> GrepDialog -> OutputWindow -> EditorWindow
|
||
|
|
||
|
|
||
|
def grep(text, io=None, flist=None):
|
||
|
"""Open the Find in Files dialog.
|
||
|
|
||
|
Module-level function to access the singleton GrepDialog
|
||
|
instance and open the dialog. If text is selected, it is
|
||
|
used as the search phrase; otherwise, the previous entry
|
||
|
is used.
|
||
|
|
||
|
Args:
|
||
|
text: Text widget that contains the selected text for
|
||
|
default search phrase.
|
||
|
io: iomenu.IOBinding instance with default path to search.
|
||
|
flist: filelist.FileList instance for OutputWindow parent.
|
||
|
"""
|
||
|
root = text._root()
|
||
|
engine = searchengine.get(root)
|
||
|
if not hasattr(engine, "_grepdialog"):
|
||
|
engine._grepdialog = GrepDialog(root, engine, flist)
|
||
|
dialog = engine._grepdialog
|
||
|
searchphrase = text.get("sel.first", "sel.last")
|
||
|
dialog.open(text, searchphrase, io)
|
||
|
|
||
|
|
||
|
def walk_error(msg):
|
||
|
"Handle os.walk error."
|
||
|
print(msg)
|
||
|
|
||
|
|
||
|
def findfiles(folder, pattern, recursive):
|
||
|
"""Generate file names in dir that match pattern.
|
||
|
|
||
|
Args:
|
||
|
folder: Root directory to search.
|
||
|
pattern: File pattern to match.
|
||
|
recursive: True to include subdirectories.
|
||
|
"""
|
||
|
for dirpath, _, filenames in os.walk(folder, onerror=walk_error):
|
||
|
yield from (os.path.join(dirpath, name)
|
||
|
for name in filenames
|
||
|
if fnmatch.fnmatch(name, pattern))
|
||
|
if not recursive:
|
||
|
break
|
||
|
|
||
|
|
||
|
class GrepDialog(SearchDialogBase):
|
||
|
"Dialog for searching multiple files."
|
||
|
|
||
|
title = "Find in Files Dialog"
|
||
|
icon = "Grep"
|
||
|
needwrapbutton = 0
|
||
|
|
||
|
def __init__(self, root, engine, flist):
|
||
|
"""Create search dialog for searching for a phrase in the file system.
|
||
|
|
||
|
Uses SearchDialogBase as the basis for the GUI and a
|
||
|
searchengine instance to prepare the search.
|
||
|
|
||
|
Attributes:
|
||
|
flist: filelist.Filelist instance for OutputWindow parent.
|
||
|
globvar: String value of Entry widget for path to search.
|
||
|
globent: Entry widget for globvar. Created in
|
||
|
create_entries().
|
||
|
recvar: Boolean value of Checkbutton widget for
|
||
|
traversing through subdirectories.
|
||
|
"""
|
||
|
super().__init__(root, engine)
|
||
|
self.flist = flist
|
||
|
self.globvar = StringVar(root)
|
||
|
self.recvar = BooleanVar(root)
|
||
|
|
||
|
def open(self, text, searchphrase, io=None):
|
||
|
"""Make dialog visible on top of others and ready to use.
|
||
|
|
||
|
Extend the SearchDialogBase open() to set the initial value
|
||
|
for globvar.
|
||
|
|
||
|
Args:
|
||
|
text: Multicall object containing the text information.
|
||
|
searchphrase: String phrase to search.
|
||
|
io: iomenu.IOBinding instance containing file path.
|
||
|
"""
|
||
|
SearchDialogBase.open(self, text, searchphrase)
|
||
|
if io:
|
||
|
path = io.filename or ""
|
||
|
else:
|
||
|
path = ""
|
||
|
dir, base = os.path.split(path)
|
||
|
head, tail = os.path.splitext(base)
|
||
|
if not tail:
|
||
|
tail = ".py"
|
||
|
self.globvar.set(os.path.join(dir, "*" + tail))
|
||
|
|
||
|
def create_entries(self):
|
||
|
"Create base entry widgets and add widget for search path."
|
||
|
SearchDialogBase.create_entries(self)
|
||
|
self.globent = self.make_entry("In files:", self.globvar)[0]
|
||
|
|
||
|
def create_other_buttons(self):
|
||
|
"Add check button to recurse down subdirectories."
|
||
|
btn = Checkbutton(
|
||
|
self.make_frame()[0], variable=self.recvar,
|
||
|
text="Recurse down subdirectories")
|
||
|
btn.pack(side="top", fill="both")
|
||
|
|
||
|
def create_command_buttons(self):
|
||
|
"Create base command buttons and add button for Search Files."
|
||
|
SearchDialogBase.create_command_buttons(self)
|
||
|
self.make_button("Search Files", self.default_command, isdef=True)
|
||
|
|
||
|
def default_command(self, event=None):
|
||
|
"""Grep for search pattern in file path. The default command is bound
|
||
|
to <Return>.
|
||
|
|
||
|
If entry values are populated, set OutputWindow as stdout
|
||
|
and perform search. The search dialog is closed automatically
|
||
|
when the search begins.
|
||
|
"""
|
||
|
prog = self.engine.getprog()
|
||
|
if not prog:
|
||
|
return
|
||
|
path = self.globvar.get()
|
||
|
if not path:
|
||
|
self.top.bell()
|
||
|
return
|
||
|
from idlelib.outwin import OutputWindow # leave here!
|
||
|
save = sys.stdout
|
||
|
try:
|
||
|
sys.stdout = OutputWindow(self.flist)
|
||
|
self.grep_it(prog, path)
|
||
|
finally:
|
||
|
sys.stdout = save
|
||
|
|
||
|
def grep_it(self, prog, path):
|
||
|
"""Search for prog within the lines of the files in path.
|
||
|
|
||
|
For the each file in the path directory, open the file and
|
||
|
search each line for the matching pattern. If the pattern is
|
||
|
found, write the file and line information to stdout (which
|
||
|
is an OutputWindow).
|
||
|
|
||
|
Args:
|
||
|
prog: The compiled, cooked search pattern.
|
||
|
path: String containing the search path.
|
||
|
"""
|
||
|
folder, filepat = os.path.split(path)
|
||
|
if not folder:
|
||
|
folder = os.curdir
|
||
|
filelist = sorted(findfiles(folder, filepat, self.recvar.get()))
|
||
|
self.close()
|
||
|
pat = self.engine.getpat()
|
||
|
print(f"Searching {pat!r} in {path} ...")
|
||
|
hits = 0
|
||
|
try:
|
||
|
for fn in filelist:
|
||
|
try:
|
||
|
with open(fn, errors='replace') as f:
|
||
|
for lineno, line in enumerate(f, 1):
|
||
|
if line[-1:] == '\n':
|
||
|
line = line[:-1]
|
||
|
if prog.search(line):
|
||
|
sys.stdout.write(f"{fn}: {lineno}: {line}\n")
|
||
|
hits += 1
|
||
|
except OSError as msg:
|
||
|
print(msg)
|
||
|
print(f"Hits found: {hits}\n(Hint: right-click to open locations.)"
|
||
|
if hits else "No hits.")
|
||
|
except AttributeError:
|
||
|
# Tk window has been closed, OutputWindow.text = None,
|
||
|
# so in OW.write, OW.text.insert fails.
|
||
|
pass
|
||
|
|
||
|
|
||
|
def _grep_dialog(parent): # htest #
|
||
|
from tkinter import Toplevel, Text, SEL, END
|
||
|
from tkinter.ttk import Frame, Button
|
||
|
from idlelib.pyshell import PyShellFileList
|
||
|
|
||
|
top = Toplevel(parent)
|
||
|
top.title("Test GrepDialog")
|
||
|
x, y = map(int, parent.geometry().split('+')[1:])
|
||
|
top.geometry(f"+{x}+{y + 175}")
|
||
|
|
||
|
flist = PyShellFileList(top)
|
||
|
frame = Frame(top)
|
||
|
frame.pack()
|
||
|
text = Text(frame, height=5)
|
||
|
text.pack()
|
||
|
|
||
|
def show_grep_dialog():
|
||
|
text.tag_add(SEL, "1.0", END)
|
||
|
grep(text, flist=flist)
|
||
|
text.tag_remove(SEL, "1.0", END)
|
||
|
|
||
|
button = Button(frame, text="Show GrepDialog", command=show_grep_dialog)
|
||
|
button.pack()
|
||
|
|
||
|
if __name__ == "__main__":
|
||
|
from unittest import main
|
||
|
main('idlelib.idle_test.test_grep', verbosity=2, exit=False)
|
||
|
|
||
|
from idlelib.idle_test.htest import run
|
||
|
run(_grep_dialog)
|