g.search.modules.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  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.utils import diff_files, try_rmdir
  68. from grass.script import core as grass
  69. from grass.exceptions import CalledModuleError
  70. try:
  71. import xml.etree.ElementTree as etree
  72. except ImportError:
  73. import elementtree.ElementTree as etree # Python <= 2.4
  74. COLORIZE = False
  75. def main():
  76. global COLORIZE
  77. AND = flags['a']
  78. NOT = flags['n']
  79. manpages = flags['m']
  80. exact_keywords = flags['k']
  81. out_format = None
  82. if flags['g']:
  83. out_format = 'shell'
  84. elif flags['j']:
  85. out_format = 'json'
  86. else:
  87. COLORIZE = flags['c']
  88. keywords = None
  89. if options['keyword']:
  90. if exact_keywords:
  91. keywords = options['keyword'].split(',')
  92. else:
  93. keywords = options['keyword'].lower().split(',')
  94. else:
  95. NOT = None
  96. modules = _search_module(keywords, AND, NOT, manpages, exact_keywords)
  97. print_results(modules, out_format)
  98. def print_results(data, out_format=None):
  99. """
  100. Print result of the searching method
  101. each data item should have
  102. {
  103. 'name': name of the item,
  104. 'attributes': {
  105. # list of attributes to be shown too
  106. }
  107. }
  108. :param list.<dict> data: input list of found data items
  109. :param str out_format: output format 'shell', 'json', None
  110. """
  111. if not out_format:
  112. _print_results(data)
  113. elif out_format == 'shell':
  114. _print_results_shell(data)
  115. elif out_format == 'json':
  116. _print_results_json(data)
  117. def _print_results_shell(data):
  118. """Print just the name attribute"""
  119. for item in data:
  120. print(item['name'])
  121. def _print_results_json(data):
  122. """Print JSON output"""
  123. import json
  124. print(json.dumps(data, sort_keys=True, indent=4, separators=(',', ': ')))
  125. def _print_results(data):
  126. import textwrap
  127. for item in data:
  128. print('\n{0}'.format(colorize(item['name'], attrs=['bold'])))
  129. for attr in item['attributes']:
  130. out = '{0}: {1}'.format(attr, item['attributes'][attr])
  131. out = textwrap.wrap(out, width=79, initial_indent=4 * ' ',
  132. subsequent_indent=4 * ' ' + len(attr) * ' ' + ' ')
  133. for line in out:
  134. print(line)
  135. def colorize(text, attrs=None, pattern=None):
  136. """Colorize given text input
  137. :param string text: input text to be colored
  138. :param list.<string> attrs: list of attributes as defined in termcolor package
  139. :param string pattern: text to be highlighted in input text
  140. :return: colored string
  141. """
  142. if COLORIZE:
  143. try:
  144. from termcolor import colored
  145. except ImportError:
  146. grass.fatal(_("Cannot colorize, python-termcolor is not installed"))
  147. else:
  148. def colored(pattern, attrs):
  149. return pattern
  150. if pattern:
  151. return text.replace(pattern, colored(pattern, attrs=attrs))
  152. else:
  153. return colored(text, attrs=attrs)
  154. def _search_module(keywords=None, logical_and=False, invert=False, manpages=False,
  155. exact_keywords=False):
  156. """Search modules by given keywords
  157. :param list.<str> keywords: list of keywords
  158. :param boolean logical_and: use AND (default OR)
  159. :param boolean manpages: search in manpages too
  160. :return dict: modules
  161. """
  162. WXGUIDIR = os.path.join(os.getenv("GISBASE"), "gui", "wxpython")
  163. filename = os.path.join(WXGUIDIR, 'xml', 'module_items.xml')
  164. menudata_file = open(filename, 'r')
  165. menudata = etree.parse(menudata_file)
  166. menudata_file.close()
  167. items = menudata.findall('module-item')
  168. # add installed addons to modules list
  169. if os.getenv("GRASS_ADDON_BASE"):
  170. filename_addons = os.path.join(os.getenv("GRASS_ADDON_BASE"), 'modules.xml')
  171. if os.path.isfile(filename_addons):
  172. addon_menudata_file = open(filename_addons, 'r')
  173. addon_menudata = etree.parse(addon_menudata_file)
  174. addon_menudata_file.close()
  175. addon_items = addon_menudata.findall('task')
  176. items.extend(addon_items)
  177. # add system-wide installed addons to modules list
  178. filename_addons_s = os.path.join(os.getenv("GISBASE"), 'modules.xml')
  179. if os.path.isfile(filename_addons_s):
  180. addon_menudata_file_s = open(filename_addons_s, 'r')
  181. addon_menudata_s = etree.parse(addon_menudata_file_s)
  182. addon_menudata_file_s.close()
  183. addon_items_s = addon_menudata_s.findall('task')
  184. items.extend(addon_items_s)
  185. found_modules = []
  186. for item in items:
  187. name = item.attrib['name']
  188. description = item.find('description').text
  189. module_keywords = item.find('keywords').text
  190. if not keywords:
  191. # list all modules
  192. found = [True]
  193. else:
  194. found = [False]
  195. if logical_and:
  196. found = [False] * len(keywords)
  197. for idx in range(len(keywords)):
  198. keyword = keywords[idx]
  199. keyword_found = False
  200. if exact_keywords:
  201. keyword_found = _exact_search(keyword, module_keywords)
  202. else:
  203. keyword_found = _basic_search(keyword, name, description,
  204. module_keywords)
  205. # meta-modules (i.sentinel, r.modis, ...) do not have descriptions
  206. # and keywords, but they have a manpage
  207. # TODO change the handling of meta-modules
  208. if (description and module_keywords) and not keyword_found and manpages:
  209. keyword_found = _manpage_search(keyword, name)
  210. if keyword_found:
  211. if logical_and:
  212. found[idx] = True
  213. else:
  214. found = [True]
  215. description = colorize(description,
  216. attrs=['underline'],
  217. pattern=keyword)
  218. module_keywords = colorize(module_keywords,
  219. attrs=['underline'],
  220. pattern=keyword)
  221. add = False not in found
  222. if invert:
  223. add = not add
  224. if add:
  225. found_modules.append({
  226. 'name': name,
  227. 'attributes': {
  228. 'keywords': module_keywords,
  229. 'description': description
  230. }
  231. })
  232. return sorted(found_modules, key=lambda k: k['name'])
  233. def _basic_search(pattern, name, description, module_keywords):
  234. """Search for a string in all the provided strings.
  235. This lowercases the strings before searching in them, so the pattern
  236. string should be lowercased too.
  237. """
  238. if (name and description and module_keywords):
  239. if name.lower().find(pattern) > -1 or\
  240. description.lower().find(pattern) > -1 or\
  241. module_keywords.lower().find(pattern) > -1:
  242. return True
  243. else:
  244. return False
  245. else:
  246. return False
  247. def _exact_search(keyword, module_keywords):
  248. """Compare exact keyword with module keywords
  249. :param keyword: exact keyword to find in the list (not lowercased)
  250. :param module_keywords: comma separated list of keywords
  251. """
  252. module_keywords = module_keywords.split(',')
  253. for current in module_keywords:
  254. if keyword == current:
  255. return True
  256. return False
  257. def _manpage_search(pattern, name):
  258. try:
  259. manpage = grass.read_command('g.manual', flags='m', entry=name)
  260. except CalledModuleError:
  261. # in case man page is missing
  262. return False
  263. return manpage.lower().find(pattern) > -1
  264. if __name__ == "__main__":
  265. options, flags = grass.parser()
  266. sys.exit(main())