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