g.search.modules.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. #!/usr/bin/env python3
  2. ############################################################################
  3. #
  4. # MODULE: g.search.modules
  5. # AUTHOR(S): Jachym Cepicky <jachym.cepicky gmail.com>
  6. # PURPOSE: g.search.modules in grass modules using keywords
  7. # COPYRIGHT: (C) 2015-2019 by the GRASS Development Team
  8. #
  9. # This program is free software under the GNU General
  10. # Public License (>=v2). Read the file COPYING that
  11. # comes with GRASS for details.
  12. #
  13. #############################################################################
  14. # %module
  15. # % description: Search in GRASS modules using keywords
  16. # % keyword: general
  17. # % keyword: modules
  18. # % keyword: search
  19. # %end
  20. # %option
  21. # % key: keyword
  22. # % multiple: yes
  23. # % type: string
  24. # % label: Keyword to be searched
  25. # % description: List all modules if not given
  26. # % required : no
  27. # %end
  28. # %flag
  29. # % key: a
  30. # % description: Display only modules where all keywords are available (AND), default: OR
  31. # % guisection: Output
  32. # %end
  33. # %flag
  34. # % key: n
  35. # % description: Invert selection (logical NOT)
  36. # % guisection: Output
  37. # %end
  38. # %flag
  39. # % key: m
  40. # % description: Search in manual pages too (can be slow)
  41. # % guisection: Output
  42. # %end
  43. # %flag
  44. # % key: k
  45. # % label: Search only for the exact keyword in module keyword list
  46. # % description: Instead of full text search, search only in actual keywords
  47. # % guisection: Output
  48. # %end
  49. # %flag
  50. # % key: c
  51. # % description: Use colorized (more readable) output to terminal
  52. # % guisection: Output
  53. # %end
  54. # %flag
  55. # % key: g
  56. # % description: Shell script format
  57. # % guisection: Output
  58. # %end
  59. # %flag
  60. # % key: j
  61. # % description: JSON format
  62. # % guisection: Output
  63. # %end
  64. from __future__ import print_function
  65. import os
  66. import sys
  67. from grass.script import core as grass
  68. from grass.exceptions import CalledModuleError
  69. try:
  70. import xml.etree.ElementTree as etree
  71. except ImportError:
  72. import elementtree.ElementTree as etree # Python <= 2.4
  73. COLORIZE = False
  74. def main():
  75. global COLORIZE
  76. AND = flags["a"]
  77. NOT = flags["n"]
  78. manpages = flags["m"]
  79. exact_keywords = flags["k"]
  80. out_format = None
  81. if flags["g"]:
  82. out_format = "shell"
  83. elif flags["j"]:
  84. out_format = "json"
  85. else:
  86. COLORIZE = flags["c"]
  87. keywords = None
  88. if options["keyword"]:
  89. if exact_keywords:
  90. keywords = options["keyword"].split(",")
  91. else:
  92. keywords = options["keyword"].lower().split(",")
  93. else:
  94. NOT = None
  95. modules = _search_module(keywords, AND, NOT, manpages, exact_keywords)
  96. print_results(modules, out_format)
  97. def print_results(data, out_format=None):
  98. """
  99. Print result of the searching method
  100. each data item should have
  101. {
  102. 'name': name of the item,
  103. 'attributes': {
  104. # list of attributes to be shown too
  105. }
  106. }
  107. :param list.<dict> data: input list of found data items
  108. :param str out_format: output format 'shell', 'json', None
  109. """
  110. if not out_format:
  111. _print_results(data)
  112. elif out_format == "shell":
  113. _print_results_shell(data)
  114. elif out_format == "json":
  115. _print_results_json(data)
  116. def _print_results_shell(data):
  117. """Print just the name attribute"""
  118. for item in data:
  119. print(item["name"])
  120. def _print_results_json(data):
  121. """Print JSON output"""
  122. import json
  123. print(json.dumps(data, sort_keys=True, indent=4, separators=(",", ": ")))
  124. def _print_results(data):
  125. import textwrap
  126. for item in data:
  127. print("\n{0}".format(colorize(item["name"], attrs=["bold"])))
  128. for attr in item["attributes"]:
  129. out = "{0}: {1}".format(attr, item["attributes"][attr])
  130. out = textwrap.wrap(
  131. out,
  132. width=79,
  133. initial_indent=4 * " ",
  134. subsequent_indent=4 * " " + len(attr) * " " + " ",
  135. )
  136. for line in out:
  137. print(line)
  138. def colorize(text, attrs=None, pattern=None):
  139. """Colorize given text input
  140. :param string text: input text to be colored
  141. :param list.<string> attrs: list of attributes as defined in termcolor package
  142. :param string pattern: text to be highlighted in input text
  143. :return: colored string
  144. """
  145. if COLORIZE:
  146. try:
  147. from termcolor import colored
  148. except ImportError:
  149. grass.fatal(_("Cannot colorize, python-termcolor is not installed"))
  150. else:
  151. def colored(pattern, attrs):
  152. return pattern
  153. if pattern:
  154. return text.replace(pattern, colored(pattern, attrs=attrs))
  155. else:
  156. return colored(text, attrs=attrs)
  157. def _search_module(
  158. keywords=None, logical_and=False, invert=False, manpages=False, exact_keywords=False
  159. ):
  160. """Search modules by given keywords
  161. :param list.<str> keywords: list of keywords
  162. :param boolean logical_and: use AND (default OR)
  163. :param boolean manpages: search in manpages too
  164. :return dict: modules
  165. """
  166. WXGUIDIR = os.path.join(os.getenv("GISBASE"), "gui", "wxpython")
  167. filename = os.path.join(WXGUIDIR, "xml", "module_items.xml")
  168. menudata_file = open(filename, "r")
  169. menudata = etree.parse(menudata_file)
  170. menudata_file.close()
  171. items = menudata.findall("module-item")
  172. # add installed addons to modules list
  173. if os.getenv("GRASS_ADDON_BASE"):
  174. filename_addons = os.path.join(os.getenv("GRASS_ADDON_BASE"), "modules.xml")
  175. if os.path.isfile(filename_addons):
  176. addon_menudata_file = open(filename_addons, "r")
  177. addon_menudata = etree.parse(addon_menudata_file)
  178. addon_menudata_file.close()
  179. addon_items = addon_menudata.findall("task")
  180. items.extend(addon_items)
  181. # add system-wide installed addons to modules list
  182. filename_addons_s = os.path.join(os.getenv("GISBASE"), "modules.xml")
  183. if os.path.isfile(filename_addons_s):
  184. addon_menudata_file_s = open(filename_addons_s, "r")
  185. addon_menudata_s = etree.parse(addon_menudata_file_s)
  186. addon_menudata_file_s.close()
  187. addon_items_s = addon_menudata_s.findall("task")
  188. items.extend(addon_items_s)
  189. found_modules = []
  190. for item in items:
  191. name = item.attrib["name"]
  192. description = item.find("description").text
  193. module_keywords = item.find("keywords").text
  194. if not keywords:
  195. # list all modules
  196. found = [True]
  197. else:
  198. found = [False]
  199. if logical_and:
  200. found = [False] * len(keywords)
  201. for idx in range(len(keywords)):
  202. keyword = keywords[idx]
  203. keyword_found = False
  204. if exact_keywords:
  205. keyword_found = _exact_search(keyword, module_keywords)
  206. else:
  207. keyword_found = _basic_search(
  208. keyword, name, description, module_keywords
  209. )
  210. # meta-modules (i.sentinel, r.modis, ...) do not have descriptions
  211. # and keywords, but they have a manpage
  212. # TODO change the handling of meta-modules
  213. if (description and module_keywords) and not keyword_found and manpages:
  214. keyword_found = _manpage_search(keyword, name)
  215. if keyword_found:
  216. if logical_and:
  217. found[idx] = True
  218. else:
  219. found = [True]
  220. description = colorize(
  221. description, attrs=["underline"], pattern=keyword
  222. )
  223. module_keywords = colorize(
  224. module_keywords, attrs=["underline"], pattern=keyword
  225. )
  226. add = False not in found
  227. if invert:
  228. add = not add
  229. if add:
  230. found_modules.append(
  231. {
  232. "name": name,
  233. "attributes": {
  234. "keywords": module_keywords,
  235. "description": description,
  236. },
  237. }
  238. )
  239. return sorted(found_modules, key=lambda k: k["name"])
  240. def _basic_search(pattern, name, description, module_keywords):
  241. """Search for a string in all the provided strings.
  242. This lowercases the strings before searching in them, so the pattern
  243. string should be lowercased too.
  244. """
  245. if name and description and module_keywords:
  246. if (
  247. name.lower().find(pattern) > -1
  248. or description.lower().find(pattern) > -1
  249. or module_keywords.lower().find(pattern) > -1
  250. ):
  251. return True
  252. else:
  253. return False
  254. else:
  255. return False
  256. def _exact_search(keyword, module_keywords):
  257. """Compare exact keyword with module keywords
  258. :param keyword: exact keyword to find in the list (not lowercased)
  259. :param module_keywords: comma separated list of keywords
  260. """
  261. module_keywords = module_keywords.split(",")
  262. for current in module_keywords:
  263. if keyword == current:
  264. return True
  265. return False
  266. def _manpage_search(pattern, name):
  267. try:
  268. manpage = grass.read_command("g.manual", flags="m", entry=name)
  269. except CalledModuleError:
  270. # in case man page is missing
  271. return False
  272. return manpage.lower().find(pattern) > -1
  273. if __name__ == "__main__":
  274. options, flags = grass.parser()
  275. sys.exit(main())