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 def search_recursive(directory, term_type, term, op=None): 27 28 """ 29 Search files within the filesystem below 'directory' for terms having the 30 given 'term_type' whose value matches the specified 'term'. 31 """ 32 33 results = [] 34 for path, directories, filenames in os.walk(args["directory"]): 35 for filename in filenames: 36 if os.path.splitext(filename)[-1] == os.path.extsep + "py": 37 results += search_file(os.path.join(path, filename), term_type, term, op) 38 return results 39 40 def search_file(filename, term_type, term, op=None): 41 42 """ 43 Search the file with the given 'filename' for terms having the given 44 'term_type' whose value matches the specified 'term'. 45 """ 46 47 node = compiler.parseFile(filename) 48 cls = getattr(compiler.ast, term_type) 49 return search_tree(node, cls, term, op, filename) 50 51 def search_tree(node, cls, term, op=None, filename=None): 52 53 """ 54 Search the tree rooted at the given 'node' for nodes of the given class 55 'cls' for content matching the specified 'term'. 56 57 Return a list of results of the form (node, value, filename). 58 """ 59 60 results = [] 61 62 if isinstance(node, cls): 63 if op is None: 64 results.append((node, None, filename)) 65 else: 66 for child in node.getChildren(): 67 if isinstance(child, (str, unicode, int, float, long, bool)) and op(unicode(child), term): 68 results.append((node, child, filename)) 69 break 70 71 # Search within nodes, even if matches have already been found. 72 73 for child in node.getChildNodes(): 74 results += search_tree(child, cls, term, op, filename) 75 76 return results 77 78 def get_operator(name): 79 80 "Return the operator having the given 'name'." 81 82 if name is None: 83 return unicode.__eq__ 84 else: 85 return getattr(unicode, name) 86 87 def expand_results(results): 88 89 """ 90 Expand the given 'results', making a list containing tuples of the form 91 (filename, line number, line, value). 92 """ 93 94 expanded = [] 95 96 for node, value, filename in results: 97 if filename is not None: 98 line = linecache.getline(filename, node.lineno).rstrip() 99 else: 100 line = None 101 102 expanded.append((filename, node.lineno, line, value)) 103 104 return expanded 105 106 # Command syntax. 107 108 syntax_description = """ 109 [ -n ] 110 [ -v ] 111 -t <term-type> 112 [ -o <operator> ] 113 [ -e <expression> ] 114 ( ( -r <directory> ) | <filename> ) 115 """ 116 117 # Main program. 118 119 if __name__ == "__main__": 120 import sys 121 import cmdsyntax 122 import linecache 123 124 # Match command arguments. 125 126 syntax = cmdsyntax.Syntax(syntax_description) 127 syntax_matches = syntax.get_args(sys.argv[1:]) 128 129 try: 130 args = syntax_matches[0] 131 except IndexError: 132 print "Syntax:" 133 print syntax_description 134 sys.exit(1) 135 136 # Get the search details. 137 138 term_type = args["term-type"] 139 term = args.get("expression") 140 141 if term is None: 142 op = None 143 else: 144 op = get_operator(args.get("operator")) 145 146 # Perform the search either in a single file or in a directory hierarchy. 147 148 if args.has_key("filename"): 149 results = search_file(args["filename"], term_type, term, op) 150 else: 151 results = search_recursive(args["directory"], term_type, term, op) 152 153 # Present the results. 154 155 for filename, lineno, line, value in expand_results(results): 156 format = "%s:" 157 output = [filename] 158 159 if args.has_key("n"): 160 format += "%d:" 161 output.append(lineno) 162 163 if args.has_key("v"): 164 if value is not None: 165 format += "%r:" 166 output.append(value) 167 else: 168 format += "%s:" 169 output.append("<%s>" % term_type) 170 171 format += " %s" 172 output.append(line) 173 174 print format % tuple(output) 175 176 # vim: tabstop=4 expandtab shiftwidth=4