grep.py 6.58 KB
Newer Older
1 2 3 4 5
"""Grep dialog for Find in Files functionality.

   Inherits from SearchDialogBase for GUI and uses searchengine
   to prepare search pattern.
"""
David Scherer's avatar
David Scherer committed
6
import fnmatch
7
import os
David Scherer's avatar
David Scherer committed
8
import sys
9

10 11
from tkinter import StringVar, BooleanVar
from tkinter.ttk import Checkbutton
12

13
from idlelib.searchbase import SearchDialogBase
14 15 16
from idlelib import searchengine

# Importing OutputWindow here fails due to import loop
17
# EditorWindow -> GrepDialop -> OutputWindow -> EditorWindow
David Scherer's avatar
David Scherer committed
18

19

David Scherer's avatar
David Scherer committed
20
def grep(text, io=None, flist=None):
21 22 23 24 25 26 27 28 29
    """Create or find singleton GrepDialog instance.

    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.
    """

David Scherer's avatar
David Scherer committed
30
    root = text._root()
31
    engine = searchengine.get(root)
David Scherer's avatar
David Scherer committed
32 33 34
    if not hasattr(engine, "_grepdialog"):
        engine._grepdialog = GrepDialog(root, engine, flist)
    dialog = engine._grepdialog
35 36
    searchphrase = text.get("sel.first", "sel.last")
    dialog.open(text, searchphrase, io)
David Scherer's avatar
David Scherer committed
37

38

David Scherer's avatar
David Scherer committed
39
class GrepDialog(SearchDialogBase):
40
    "Dialog for searching multiple files."
David Scherer's avatar
David Scherer committed
41 42 43 44 45 46

    title = "Find in Files Dialog"
    icon = "Grep"
    needwrapbutton = 0

    def __init__(self, root, engine, flist):
47 48 49 50 51 52 53 54 55 56
        """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:
            globvar: Value of Text Entry widget for path to search.
            recvar: Boolean value of Checkbutton widget
                    for traversing through subdirectories.
        """
David Scherer's avatar
David Scherer committed
57 58 59 60 61
        SearchDialogBase.__init__(self, root, engine)
        self.flist = flist
        self.globvar = StringVar(root)
        self.recvar = BooleanVar(root)

62
    def open(self, text, searchphrase, io=None):
63
        "Make dialog visible on top of others and ready to use."
64
        SearchDialogBase.open(self, text, searchphrase)
David Scherer's avatar
David Scherer committed
65 66 67 68 69 70 71 72 73 74 75
        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):
76
        "Create base entry widgets and add widget for search path."
David Scherer's avatar
David Scherer committed
77
        SearchDialogBase.create_entries(self)
78
        self.globent = self.make_entry("In files:", self.globvar)[0]
David Scherer's avatar
David Scherer committed
79 80

    def create_other_buttons(self):
81
        "Add check button to recurse down subdirectories."
82 83
        btn = Checkbutton(
                self.make_frame()[0], variable=self.recvar,
David Scherer's avatar
David Scherer committed
84 85 86 87
                text="Recurse down subdirectories")
        btn.pack(side="top", fill="both")

    def create_command_buttons(self):
88
        "Create base command buttons and add button for search."
David Scherer's avatar
David Scherer committed
89 90 91 92
        SearchDialogBase.create_command_buttons(self)
        self.make_button("Search Files", self.default_command, 1)

    def default_command(self, event=None):
93 94 95 96 97 98 99
        """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.
        """
David Scherer's avatar
David Scherer committed
100 101 102 103 104 105 106
        prog = self.engine.getprog()
        if not prog:
            return
        path = self.globvar.get()
        if not path:
            self.top.bell()
            return
107
        from idlelib.outwin import OutputWindow  # leave here!
David Scherer's avatar
David Scherer committed
108 109 110 111 112 113 114 115
        save = sys.stdout
        try:
            sys.stdout = OutputWindow(self.flist)
            self.grep_it(prog, path)
        finally:
            sys.stdout = save

    def grep_it(self, prog, path):
116 117 118 119 120 121 122
        """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).
        """
David Scherer's avatar
David Scherer committed
123 124 125 126 127
        dir, base = os.path.split(path)
        list = self.findfiles(dir, base, self.recvar.get())
        list.sort()
        self.close()
        pat = self.engine.getpat()
128
        print(f"Searching {pat!r} in {path} ...")
David Scherer's avatar
David Scherer committed
129
        hits = 0
130 131 132 133 134 135 136 137
        try:
            for fn in list:
                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):
138
                                sys.stdout.write(f"{fn}: {lineno}: {line}\n")
139 140 141
                                hits += 1
                except OSError as msg:
                    print(msg)
142 143
            print(f"Hits found: {hits}\n(Hint: right-click to open locations.)"
                  if hits else "No hits.")
144 145 146 147
        except AttributeError:
            # Tk window has been closed, OutputWindow.text = None,
            # so in OW.write, OW.text.insert fails.
            pass
David Scherer's avatar
David Scherer committed
148 149

    def findfiles(self, dir, base, rec):
150 151 152 153
        """Return list of files in the dir that match the base pattern.

        If rec is True, recursively iterate through subdirectories.
        """
David Scherer's avatar
David Scherer committed
154 155
        try:
            names = os.listdir(dir or os.curdir)
156
        except OSError as msg:
157
            print(msg)
David Scherer's avatar
David Scherer committed
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172
            return []
        list = []
        subdirs = []
        for name in names:
            fn = os.path.join(dir, name)
            if os.path.isdir(fn):
                subdirs.append(fn)
            else:
                if fnmatch.fnmatch(name, base):
                    list.append(fn)
        if rec:
            for subdir in subdirs:
                list.extend(self.findfiles(subdir, base, rec))
        return list

173

174
def _grep_dialog(parent):  # htest #
175 176
    from tkinter import Toplevel, Text, SEL, END
    from tkinter.ttk import Button
177
    from idlelib.pyshell import PyShellFileList
178 179
    top = Toplevel(parent)
    top.title("Test GrepDialog")
180
    x, y = map(int, parent.geometry().split('+')[1:])
181
    top.geometry(f"+{x}+{y + 175}")
182

183 184
    flist = PyShellFileList(top)
    text = Text(top, height=5)
185 186 187 188 189 190 191
    text.pack()

    def show_grep_dialog():
        text.tag_add(SEL, "1.0", END)
        grep(text, flist=flist)
        text.tag_remove(SEL, "1.0", END)

192
    button = Button(top, text="Show GrepDialog", command=show_grep_dialog)
193 194
    button.pack()

195
if __name__ == "__main__":
196 197
    from unittest import main
    main('idlelib.idle_test.test_grep', verbosity=2, exit=False)
198 199 200

    from idlelib.idle_test.htest import run
    run(_grep_dialog)