#!/usr/bin/env python3 ############################################################################ # # MODULE: g.search.modules # AUTHOR(S): Jachym Cepicky # PURPOSE: g.search.modules in grass modules using keywords # COPYRIGHT: (C) 2015-2019 by the GRASS Development Team # # This program is free software under the GNU General # Public License (>=v2). Read the file COPYING that # comes with GRASS for details. # ############################################################################# # %module # % description: Search in GRASS modules using keywords # % keyword: general # % keyword: modules # % keyword: search # %end # %option # % key: keyword # % multiple: yes # % type: string # % label: Keyword to be searched # % description: List all modules if not given # % required : no # %end # %flag # % key: a # % description: Display only modules where all keywords are available (AND), default: OR # % guisection: Output # %end # %flag # % key: n # % description: Invert selection (logical NOT) # % guisection: Output # %end # %flag # % key: m # % description: Search in manual pages too (can be slow) # % guisection: Output # %end # %flag # % key: k # % label: Search only for the exact keyword in module keyword list # % description: Instead of full text search, search only in actual keywords # % guisection: Output # %end # %flag # % key: c # % description: Use colorized (more readable) output to terminal # % guisection: Output # %end # %flag # % key: g # % description: Shell script format # % guisection: Output # %end # %flag # % key: j # % description: JSON format # % guisection: Output # %end from __future__ import print_function import os import sys from grass.script import core as grass from grass.exceptions import CalledModuleError try: import xml.etree.ElementTree as etree except ImportError: import elementtree.ElementTree as etree # Python <= 2.4 COLORIZE = False def main(): global COLORIZE AND = flags["a"] NOT = flags["n"] manpages = flags["m"] exact_keywords = flags["k"] out_format = None if flags["g"]: out_format = "shell" elif flags["j"]: out_format = "json" else: COLORIZE = flags["c"] keywords = None if options["keyword"]: if exact_keywords: keywords = options["keyword"].split(",") else: keywords = options["keyword"].lower().split(",") else: NOT = None modules = _search_module(keywords, AND, NOT, manpages, exact_keywords) print_results(modules, out_format) def print_results(data, out_format=None): """ Print result of the searching method each data item should have { 'name': name of the item, 'attributes': { # list of attributes to be shown too } } :param list. data: input list of found data items :param str out_format: output format 'shell', 'json', None """ if not out_format: _print_results(data) elif out_format == "shell": _print_results_shell(data) elif out_format == "json": _print_results_json(data) def _print_results_shell(data): """Print just the name attribute""" for item in data: print(item["name"]) def _print_results_json(data): """Print JSON output""" import json print(json.dumps(data, sort_keys=True, indent=4, separators=(",", ": "))) def _print_results(data): import textwrap for item in data: print("\n{0}".format(colorize(item["name"], attrs=["bold"]))) for attr in item["attributes"]: out = "{0}: {1}".format(attr, item["attributes"][attr]) out = textwrap.wrap( out, width=79, initial_indent=4 * " ", subsequent_indent=4 * " " + len(attr) * " " + " ", ) for line in out: print(line) def colorize(text, attrs=None, pattern=None): """Colorize given text input :param string text: input text to be colored :param list. attrs: list of attributes as defined in termcolor package :param string pattern: text to be highlighted in input text :return: colored string """ if COLORIZE: try: from termcolor import colored except ImportError: grass.fatal(_("Cannot colorize, python-termcolor is not installed")) else: def colored(pattern, attrs): return pattern if pattern: return text.replace(pattern, colored(pattern, attrs=attrs)) else: return colored(text, attrs=attrs) def _search_module( keywords=None, logical_and=False, invert=False, manpages=False, exact_keywords=False ): """Search modules by given keywords :param list. keywords: list of keywords :param boolean logical_and: use AND (default OR) :param boolean manpages: search in manpages too :return dict: modules """ WXGUIDIR = os.path.join(os.getenv("GISBASE"), "gui", "wxpython") filename = os.path.join(WXGUIDIR, "xml", "module_items.xml") menudata_file = open(filename, "r") menudata = etree.parse(menudata_file) menudata_file.close() items = menudata.findall("module-item") # add installed addons to modules list if os.getenv("GRASS_ADDON_BASE"): filename_addons = os.path.join(os.getenv("GRASS_ADDON_BASE"), "modules.xml") if os.path.isfile(filename_addons): addon_menudata_file = open(filename_addons, "r") addon_menudata = etree.parse(addon_menudata_file) addon_menudata_file.close() addon_items = addon_menudata.findall("task") items.extend(addon_items) # add system-wide installed addons to modules list filename_addons_s = os.path.join(os.getenv("GISBASE"), "modules.xml") if os.path.isfile(filename_addons_s): addon_menudata_file_s = open(filename_addons_s, "r") addon_menudata_s = etree.parse(addon_menudata_file_s) addon_menudata_file_s.close() addon_items_s = addon_menudata_s.findall("task") items.extend(addon_items_s) found_modules = [] for item in items: name = item.attrib["name"] description = item.find("description").text module_keywords = item.find("keywords").text if not keywords: # list all modules found = [True] else: found = [False] if logical_and: found = [False] * len(keywords) for idx in range(len(keywords)): keyword = keywords[idx] keyword_found = False if exact_keywords: keyword_found = _exact_search(keyword, module_keywords) else: keyword_found = _basic_search( keyword, name, description, module_keywords ) # meta-modules (i.sentinel, r.modis, ...) do not have descriptions # and keywords, but they have a manpage # TODO change the handling of meta-modules if (description and module_keywords) and not keyword_found and manpages: keyword_found = _manpage_search(keyword, name) if keyword_found: if logical_and: found[idx] = True else: found = [True] description = colorize( description, attrs=["underline"], pattern=keyword ) module_keywords = colorize( module_keywords, attrs=["underline"], pattern=keyword ) add = False not in found if invert: add = not add if add: found_modules.append( { "name": name, "attributes": { "keywords": module_keywords, "description": description, }, } ) return sorted(found_modules, key=lambda k: k["name"]) def _basic_search(pattern, name, description, module_keywords): """Search for a string in all the provided strings. This lowercases the strings before searching in them, so the pattern string should be lowercased too. """ if name and description and module_keywords: if ( name.lower().find(pattern) > -1 or description.lower().find(pattern) > -1 or module_keywords.lower().find(pattern) > -1 ): return True else: return False else: return False def _exact_search(keyword, module_keywords): """Compare exact keyword with module keywords :param keyword: exact keyword to find in the list (not lowercased) :param module_keywords: comma separated list of keywords """ module_keywords = module_keywords.split(",") for current in module_keywords: if keyword == current: return True return False def _manpage_search(pattern, name): try: manpage = grass.read_command("g.manual", flags="m", entry=name) except CalledModuleError: # in case man page is missing return False return manpage.lower().find(pattern) > -1 if __name__ == "__main__": options, flags = grass.parser() sys.exit(main())