1 #!/usr/bin/env python 2 3 """ 4 Search Python abstract syntax trees for nodes of a particular type having a 5 particular textual value. 6 7 Copyright (C) 2008 Paul Boddie <paul@boddie.org.uk> 8 9 This program is free software; you can redistribute it and/or modify it under 10 the terms of the GNU General Public License as published by the Free Software 11 Foundation; either version 3 of the License, or (at your option) any later 12 version. 13 14 This program is distributed in the hope that it will be useful, but WITHOUT 15 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 16 FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 17 details. 18 19 You should have received a copy of the GNU General Public License along with 20 this program. If not, see <http://www.gnu.org/licenses/>. 21 """ 22 23 import compiler 24 import os 25 26 __version__ = "0.1" 27 28 def search_recursive(directory, term_type, term, op=None): 29 30 """ 31 Search files within the filesystem below 'directory' for terms having the 32 given 'term_type' whose value matches the specified 'term'. 33 """ 34 35 results = [] 36 for path, directories, filenames in os.walk(directory): 37 for filename in filenames: 38 if os.path.splitext(filename)[-1] == os.path.extsep + "py": 39 results += search_file(os.path.join(path, filename), term_type, term, op) 40 return results 41 42 def search_file(filename, term_type, term, op=None): 43 44 """ 45 Search the file with the given 'filename' for terms having the given 46 'term_type' whose value matches the specified 'term'. 47 """ 48 49 node = compiler.parseFile(filename) 50 cls = getattr(compiler.ast, term_type) 51 return search_tree(node, cls, term, op, filename) 52 53 def search_tree(node, cls, term, op=None, filename=None): 54 55 """ 56 Search the tree rooted at the given 'node' for nodes of the given class 57 'cls' for content matching the specified 'term'. 58 59 Return a list of results of the form (node, value, filename). 60 """ 61 62 results = [] 63 64 if isinstance(node, cls): 65 if op is None: 66 results.append((node, None, filename)) 67 else: 68 for child in node.getChildren(): 69 if isinstance(child, (str, unicode, int, float, long, bool)) and op(unicode(child)): 70 results.append((node, child, filename)) 71 break 72 73 # Search within nodes, even if matches have already been found. 74 75 for child in node.getChildNodes(): 76 results += search_tree(child, cls, term, op, filename) 77 78 return results 79 80 def expand_results(results): 81 82 """ 83 Expand the given 'results', making a list containing tuples of the form 84 (filename, line number, line, value). 85 """ 86 87 expanded = [] 88 89 for node, value, filename in results: 90 if filename is not None: 91 line = linecache.getline(filename, node.lineno).rstrip() 92 else: 93 line = None 94 95 expanded.append((filename, node.lineno, line, value)) 96 97 return expanded 98 99 # Command syntax. 100 101 syntax_description = """ 102 [ -n | --line-number ] 103 [ -p | --print-token ] 104 ( ( -t TERM_TYPE ) | ( --type=TERM_TYPE ) ) 105 [ ( -e PATTERN ) | ( --regexp=PATTERN ) ] 106 ( ( ( -r | -R | --recursive ) DIRECTORY ) | FILENAME ) 107 """ 108 109 # Main program. 110 111 if __name__ == "__main__": 112 import sys 113 import cmdsyntax 114 import re 115 import linecache 116 117 # Match command arguments. 118 119 syntax = cmdsyntax.Syntax(syntax_description) 120 syntax_matches = syntax.get_args(sys.argv[1:]) 121 122 try: 123 args = syntax_matches[0] 124 except IndexError: 125 print "Syntax:" 126 print syntax_description 127 sys.exit(1) 128 129 # Get the search details. 130 131 term_type = args["TERM_TYPE"] 132 term = args.get("PATTERN") 133 134 if term is None: 135 op = None 136 else: 137 op = re.compile(term).match 138 139 # Perform the search either in a single file or in a directory hierarchy. 140 141 if args.has_key("FILENAME"): 142 results = search_file(args["FILENAME"], term_type, term, op) 143 else: 144 results = search_recursive(args["DIRECTORY"], term_type, term, op) 145 146 # Present the results. 147 148 for filename, lineno, line, value in expand_results(results): 149 format = "%s:" 150 output = [filename] 151 152 if args.has_key("n") or args.has_key("line-number"): 153 format += "%d:" 154 output.append(lineno) 155 156 if args.has_key("p"): 157 if value is not None: 158 format += "%r:" 159 output.append(value) 160 else: 161 format += "%s:" 162 output.append("<%s>" % term_type) 163 164 format += " %s" 165 output.append(line) 166 167 print format % tuple(output) 168 169 # vim: tabstop=4 expandtab shiftwidth=4