xpathgrep.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. #!/usr/bin/env python
  2. try:
  3. import lxml.etree as et
  4. except ImportError, e:
  5. import sys
  6. print >> sys.stderr, "ERR: %s." % e
  7. sys.exit(5)
  8. import sys, os.path, optparse, itertools
  9. SHORT_DESCRIPTION = "An XPath file finder for XML files."
  10. __doc__ = SHORT_DESCRIPTION + '''
  11. Evaluates an XPath expression against a series of files and prints the
  12. matching subtrees to stdout.
  13. Examples::
  14. $ cat test.xml
  15. <root>
  16. <a num="1234" notnum="1234abc"/>
  17. <b text="abc"/>
  18. <c text="aBc"/>
  19. <d xmlns="http://www.example.org/ns/example" num="2"/>
  20. <d xmlns="http://www.example.org/ns/example" num="4"/>
  21. </root>
  22. # find all leaf elements:
  23. $ SCRIPT '//*[not(*)]' test.xml
  24. <a num="1234" notnum="1234abc"/>
  25. <b text="abc"/>
  26. <c text="aBc"/>
  27. # find all elements with attribute values containing "abc" ignoring case:
  28. $ SCRIPT '//*[@*[contains(py:lower(.), "abc")]]' test.xml
  29. <a num="1234" notnum="1234abc"/>
  30. <b text="abc"/>
  31. <c text="aBc"/>
  32. # find all numeric attribute values:
  33. $ SCRIPT '//@*[re:match(., "^[0-9]+$")]' test.xml
  34. 1234
  35. * find all elements with numeric attribute values:
  36. $ SCRIPT '//*[@*[re:match(., "^[0-9]+$")]]' test.xml
  37. <a num="1234" notnum="1234abc"/>
  38. * find all elements with numeric attribute values in more than one file:
  39. $ SCRIPT '//*[@*[re:match(., "^[0-9]+$")]]' test.xml test.xml test.xml
  40. >> test.xml
  41. <a num="1234" notnum="1234abc"/>
  42. >> test.xml
  43. <a num="1234" notnum="1234abc"/>
  44. >> test.xml
  45. <a num="1234" notnum="1234abc"/>
  46. * find XML files that have non-empty root nodes:
  47. $ SCRIPT -q '*' test.xml test.xml test.xml
  48. >> test.xml
  49. >> test.xml
  50. >> test.xml
  51. * find out if an XML file has at most depth three:
  52. $ SCRIPT 'not(/*/*/*)' test.xml
  53. True
  54. * find all elements that belong to a specific namespace and have @num=2
  55. $ SCRIPT --ns e=http://www.example.org/ns/example '//e:*[@num="2"]' test.xml
  56. <d xmlns="http://www.example.org/ns/example" num="2"/>
  57. By default, all Python builtins and string methods are available as
  58. XPath functions through the ``py`` prefix. There is also a string
  59. comparison function ``py:within(x, a, b)`` that tests the string x for
  60. being lexicographically within the interval ``a <= x <= b``.
  61. '''.replace('SCRIPT', os.path.basename(sys.argv[0]))
  62. REGEXP_NS = "http://exslt.org/regular-expressions"
  63. PYTHON_BUILTINS_NS = "PYTHON-BUILTINS"
  64. parser = et.XMLParser(remove_blank_text=True)
  65. def print_result(result, pretty_print):
  66. if et.iselement(result):
  67. result = et.tostring(result, xml_declaration=False,
  68. pretty_print=pretty_print)
  69. if pretty_print:
  70. result = result[:-1] # strip newline at the end
  71. print result
  72. def print_results(results, pretty_print):
  73. if isinstance(results, list):
  74. for result in results:
  75. print_result(result, pretty_print)
  76. else:
  77. print_result(results, pretty_print)
  78. def find_in_file(f, xpath, print_name=True, xinclude=False, pretty_print=True):
  79. if hasattr(f, 'name'):
  80. filename = f.name
  81. else:
  82. filename = f
  83. try:
  84. try:
  85. tree = et.parse(f, parser)
  86. except IOError, e:
  87. print >> sys.stderr, "ERR: parsing %r failed: %s: %s" % (
  88. filename, e.__class__.__name__, e)
  89. return False
  90. try:
  91. if xinclude:
  92. tree.xinclude()
  93. except IOError, e:
  94. print >> sys.stderr, "ERR: XInclude for %r failed: %s: %s" % (
  95. filename, e.__class__.__name__, e)
  96. return False
  97. if not callable(xpath):
  98. xpath = et.XPath(xpath)
  99. results = xpath(tree)
  100. if results == []:
  101. return False
  102. if print_name:
  103. print ">> %s" % f
  104. if options.verbose:
  105. print_results(results, pretty_print)
  106. return True
  107. except Exception, e:
  108. print >> sys.stderr, "ERR: %r: %s: %s" % (
  109. filename, e.__class__.__name__, e)
  110. return False
  111. def register_builtins():
  112. ns = et.FunctionNamespace(PYTHON_BUILTINS_NS)
  113. tostring = et.tostring
  114. str_xpath = et.XPath("string()")
  115. def make_string(s):
  116. if isinstance(s, list):
  117. if not s:
  118. return u''
  119. s = s[0]
  120. if not isinstance(s, unicode):
  121. if et.iselement(s):
  122. s = tostring(s, method="text", encoding=unicode)
  123. else:
  124. s = unicode(s)
  125. return s
  126. def wrap_builtin(b):
  127. def wrapped_builtin(_, *args):
  128. return b(*args)
  129. return wrapped_builtin
  130. for (name, builtin) in vars(__builtins__).iteritems():
  131. if callable(builtin):
  132. if not name.startswith('_') and name == name.lower():
  133. ns[name] = wrap_builtin(builtin)
  134. def wrap_str_method(b):
  135. def wrapped_method(_, *args):
  136. args = tuple(map(make_string, args))
  137. return b(*args)
  138. return wrapped_method
  139. for (name, method) in vars(unicode).iteritems():
  140. if callable(method):
  141. if not name.startswith('_'):
  142. ns[name] = wrap_str_method(method)
  143. def within(_, s, a, b):
  144. return make_string(a) <= make_string(s) <= make_string(b)
  145. ns["within"] = within
  146. def parse_options():
  147. from optparse import OptionParser
  148. usage = "usage: %prog [options] XPATH [FILE ...]"
  149. parser = OptionParser(
  150. usage = usage,
  151. version = "%prog using lxml.etree " + et.__version__,
  152. description = SHORT_DESCRIPTION)
  153. parser.add_option("-H", "--long-help",
  154. action="store_true", dest="long_help", default=False,
  155. help="a longer help text including usage examples")
  156. parser.add_option("-i", "--xinclude",
  157. action="store_true", dest="xinclude", default=False,
  158. help="run XInclude on the file before XPath")
  159. parser.add_option("--no-python",
  160. action="store_false", dest="python", default=True,
  161. help="disable Python builtins and functions (prefix 'py')")
  162. parser.add_option("--no-regexp",
  163. action="store_false", dest="regexp", default=True,
  164. help="disable regular expressions (prefix 're')")
  165. parser.add_option("-q", "--quiet",
  166. action="store_false", dest="verbose", default=True,
  167. help="don't print status messages to stdout")
  168. parser.add_option("-p", "--plain",
  169. action="store_false", dest="pretty_print", default=True,
  170. help="do not pretty-print the output")
  171. parser.add_option("-N", "--ns",
  172. action="append", default=[],
  173. dest="namespaces",
  174. help="add a namespace declaration: --ns PREFIX=NS",)
  175. options, args = parser.parse_args()
  176. if options.long_help:
  177. parser.print_help()
  178. print __doc__[__doc__.find('\n\n')+1:]
  179. sys.exit(0)
  180. if len(args) < 1:
  181. parser.error("first argument must be an XPath expression")
  182. return options, args
  183. def main(options, args):
  184. namespaces = {}
  185. if options.regexp:
  186. namespaces["re"] = REGEXP_NS
  187. if options.python:
  188. register_builtins()
  189. namespaces["py"] = PYTHON_BUILTINS_NS
  190. for ns in options.namespaces:
  191. prefix, NS = ns.split("=", 1)
  192. namespaces[prefix.strip()] = NS.strip()
  193. xpath = et.XPath(args[0], namespaces=namespaces)
  194. found = False
  195. if len(args) == 1:
  196. found = find_in_file(
  197. sys.stdin, xpath, False, options.xinclude,
  198. options.pretty_print)
  199. else:
  200. print_name = len(args) > 2
  201. for filename in itertools.islice(args, 1, None):
  202. found |= find_in_file(
  203. filename, xpath, print_name, options.xinclude,
  204. options.pretty_print)
  205. return found
  206. if __name__ == "__main__":
  207. try:
  208. options, args = parse_options()
  209. found = main(options, args)
  210. if found:
  211. sys.exit(0)
  212. else:
  213. sys.exit(1)
  214. except et.XPathSyntaxError, e:
  215. print >> sys.stderr, "Err: %s" % e
  216. sys.exit(4)
  217. except KeyboardInterrupt:
  218. pass