| 1 | """
|
|---|
| 2 | Get interface description of GRASS commands
|
|---|
| 3 |
|
|---|
| 4 | Based on gui/wxpython/gui_modules/menuform.py
|
|---|
| 5 |
|
|---|
| 6 | Usage:
|
|---|
| 7 |
|
|---|
| 8 | ::
|
|---|
| 9 |
|
|---|
| 10 | from grass.script import task as gtask
|
|---|
| 11 | gtask.command_info('r.info')
|
|---|
| 12 |
|
|---|
| 13 | (C) 2011 by the GRASS Development Team
|
|---|
| 14 | This program is free software under the GNU General Public
|
|---|
| 15 | License (>=v2). Read the file COPYING that comes with GRASS
|
|---|
| 16 | for details.
|
|---|
| 17 |
|
|---|
| 18 | .. sectionauthor:: Martin Landa <landa.martin gmail.com>
|
|---|
| 19 | """
|
|---|
| 20 | import re
|
|---|
| 21 | import sys
|
|---|
| 22 | import string
|
|---|
| 23 |
|
|---|
| 24 | if sys.version_info.major == 3:
|
|---|
| 25 | unicode = str
|
|---|
| 26 |
|
|---|
| 27 | try:
|
|---|
| 28 | import xml.etree.ElementTree as etree
|
|---|
| 29 | except ImportError:
|
|---|
| 30 | import elementtree.ElementTree as etree # Python <= 2.4
|
|---|
| 31 | from xml.parsers import expat # TODO: works for any Python?
|
|---|
| 32 | # Get the XML parsing exceptions to catch. The behavior chnaged with Python 2.7
|
|---|
| 33 | # and ElementTree 1.3.
|
|---|
| 34 | if hasattr(etree, 'ParseError'):
|
|---|
| 35 | ETREE_EXCEPTIONS = (etree.ParseError, expat.ExpatError)
|
|---|
| 36 | else:
|
|---|
| 37 | ETREE_EXCEPTIONS = (expat.ExpatError)
|
|---|
| 38 |
|
|---|
| 39 | from .utils import encode, decode, split
|
|---|
| 40 | from .core import *
|
|---|
| 41 |
|
|---|
| 42 |
|
|---|
| 43 | class grassTask:
|
|---|
| 44 | """This class holds the structures needed for filling by the parser
|
|---|
| 45 |
|
|---|
| 46 | Parameter blackList is a dictionary with fixed structure, eg.
|
|---|
| 47 |
|
|---|
| 48 | ::
|
|---|
| 49 |
|
|---|
| 50 | blackList = {'items' : {'d.legend' : { 'flags' : ['m'], 'params' : [] }},
|
|---|
| 51 | 'enabled': True}
|
|---|
| 52 |
|
|---|
| 53 | :param str path: full path
|
|---|
| 54 | :param blackList: hide some options in the GUI (dictionary)
|
|---|
| 55 | """
|
|---|
| 56 | def __init__(self, path=None, blackList=None):
|
|---|
| 57 | self.path = path
|
|---|
| 58 | self.name = _('unknown')
|
|---|
| 59 | self.params = list()
|
|---|
| 60 | self.description = ''
|
|---|
| 61 | self.label = ''
|
|---|
| 62 | self.flags = list()
|
|---|
| 63 | self.keywords = list()
|
|---|
| 64 | self.errorMsg = ''
|
|---|
| 65 | self.firstParam = None
|
|---|
| 66 | if blackList:
|
|---|
| 67 | self.blackList = blackList
|
|---|
| 68 | else:
|
|---|
| 69 | self.blackList = {'enabled': False, 'items': {}}
|
|---|
| 70 |
|
|---|
| 71 | if path is not None:
|
|---|
| 72 | try:
|
|---|
| 73 | processTask(tree=etree.fromstring(get_interface_description(path)),
|
|---|
| 74 | task=self)
|
|---|
| 75 | except ScriptError as e:
|
|---|
| 76 | self.errorMsg = e.value
|
|---|
| 77 |
|
|---|
| 78 | self.define_first()
|
|---|
| 79 |
|
|---|
| 80 | def define_first(self):
|
|---|
| 81 | """Define first parameter
|
|---|
| 82 |
|
|---|
| 83 | :return: name of first parameter
|
|---|
| 84 | """
|
|---|
| 85 | if len(self.params) > 0:
|
|---|
| 86 | self.firstParam = self.params[0]['name']
|
|---|
| 87 |
|
|---|
| 88 | return self.firstParam
|
|---|
| 89 |
|
|---|
| 90 | def get_error_msg(self):
|
|---|
| 91 | """Get error message ('' for no error)
|
|---|
| 92 | """
|
|---|
| 93 | return self.errorMsg
|
|---|
| 94 |
|
|---|
| 95 | def get_name(self):
|
|---|
| 96 | """Get task name
|
|---|
| 97 | """
|
|---|
| 98 | if sys.platform == 'win32':
|
|---|
| 99 | name, ext = os.path.splitext(self.name)
|
|---|
| 100 | if ext in ('.py', '.sh'):
|
|---|
| 101 | return name
|
|---|
| 102 | else:
|
|---|
| 103 | return self.name
|
|---|
| 104 |
|
|---|
| 105 | return self.name
|
|---|
| 106 |
|
|---|
| 107 | def get_description(self, full=True):
|
|---|
| 108 | """Get module's description
|
|---|
| 109 |
|
|---|
| 110 | :param bool full: True for label + desc
|
|---|
| 111 | """
|
|---|
| 112 | if self.label:
|
|---|
| 113 | if full:
|
|---|
| 114 | return self.label + ' ' + self.description
|
|---|
| 115 | else:
|
|---|
| 116 | return self.label
|
|---|
| 117 | else:
|
|---|
| 118 | return self.description
|
|---|
| 119 |
|
|---|
| 120 | def get_keywords(self):
|
|---|
| 121 | """Get module's keywords
|
|---|
| 122 | """
|
|---|
| 123 | return self.keywords
|
|---|
| 124 |
|
|---|
| 125 | def get_list_params(self, element='name'):
|
|---|
| 126 | """Get list of parameters
|
|---|
| 127 |
|
|---|
| 128 | :param str element: element name
|
|---|
| 129 | """
|
|---|
| 130 | params = []
|
|---|
| 131 | for p in self.params:
|
|---|
| 132 | params.append(p[element])
|
|---|
| 133 |
|
|---|
| 134 | return params
|
|---|
| 135 |
|
|---|
| 136 | def get_list_flags(self, element='name'):
|
|---|
| 137 | """Get list of flags
|
|---|
| 138 |
|
|---|
| 139 | :param str element: element name
|
|---|
| 140 | """
|
|---|
| 141 | flags = []
|
|---|
| 142 | for p in self.flags:
|
|---|
| 143 | flags.append(p[element])
|
|---|
| 144 |
|
|---|
| 145 | return flags
|
|---|
| 146 |
|
|---|
| 147 | def get_param(self, value, element='name', raiseError=True):
|
|---|
| 148 | """Find and return a param by name
|
|---|
| 149 |
|
|---|
| 150 | :param value: param's value
|
|---|
| 151 | :param str element: element name
|
|---|
| 152 | :param bool raiseError: True for raise on error
|
|---|
| 153 | """
|
|---|
| 154 | for p in self.params:
|
|---|
| 155 | val = p.get(element, None)
|
|---|
| 156 | if val is None:
|
|---|
| 157 | continue
|
|---|
| 158 | if isinstance(val, (list, tuple)):
|
|---|
| 159 | if value in val:
|
|---|
| 160 | return p
|
|---|
| 161 | elif isinstance(val, (bytes, unicode)):
|
|---|
| 162 | if p[element][:len(value)] == value:
|
|---|
| 163 | return p
|
|---|
| 164 | else:
|
|---|
| 165 | if p[element] == value:
|
|---|
| 166 | return p
|
|---|
| 167 |
|
|---|
| 168 | if raiseError:
|
|---|
| 169 | raise ValueError(_("Parameter element '%(element)s' not found: '%(value)s'") % \
|
|---|
| 170 | { 'element' : element, 'value' : value })
|
|---|
| 171 | else:
|
|---|
| 172 | return None
|
|---|
| 173 |
|
|---|
| 174 | def get_flag(self, aFlag):
|
|---|
| 175 | """Find and return a flag by name
|
|---|
| 176 |
|
|---|
| 177 | Raises ValueError when the flag is not found.
|
|---|
| 178 |
|
|---|
| 179 | :param str aFlag: name of the flag
|
|---|
| 180 | """
|
|---|
| 181 | for f in self.flags:
|
|---|
| 182 | if f['name'] == aFlag:
|
|---|
| 183 | return f
|
|---|
| 184 | raise ValueError(_("Flag not found: %s") % aFlag)
|
|---|
| 185 |
|
|---|
| 186 | def get_cmd_error(self):
|
|---|
| 187 | """Get error string produced by get_cmd(ignoreErrors = False)
|
|---|
| 188 |
|
|---|
| 189 | :return: list of errors
|
|---|
| 190 | """
|
|---|
| 191 | errorList = list()
|
|---|
| 192 | # determine if suppress_required flag is given
|
|---|
| 193 | for f in self.flags:
|
|---|
| 194 | if f['value'] and f['suppress_required']:
|
|---|
| 195 | return errorList
|
|---|
| 196 |
|
|---|
| 197 | for p in self.params:
|
|---|
| 198 | if not p.get('value', '') and p.get('required', False):
|
|---|
| 199 | if not p.get('default', ''):
|
|---|
| 200 | desc = p.get('label', '')
|
|---|
| 201 | if not desc:
|
|---|
| 202 | desc = p['description']
|
|---|
| 203 | errorList.append(_("Parameter '%(name)s' (%(desc)s) is missing.") % \
|
|---|
| 204 | {'name': p['name'], 'desc': encode(desc)})
|
|---|
| 205 |
|
|---|
| 206 | return errorList
|
|---|
| 207 |
|
|---|
| 208 | def get_cmd(self, ignoreErrors=False, ignoreRequired=False,
|
|---|
| 209 | ignoreDefault=True):
|
|---|
| 210 | """Produce an array of command name and arguments for feeding
|
|---|
| 211 | into some execve-like command processor.
|
|---|
| 212 |
|
|---|
| 213 | :param bool ignoreErrors: True to return whatever has been built so
|
|---|
| 214 | far, even though it would not be a correct
|
|---|
| 215 | command for GRASS
|
|---|
| 216 | :param bool ignoreRequired: True to ignore required flags, otherwise
|
|---|
| 217 | '@<required@>' is shown
|
|---|
| 218 | :param bool ignoreDefault: True to ignore parameters with default values
|
|---|
| 219 | """
|
|---|
| 220 | cmd = [self.get_name()]
|
|---|
| 221 |
|
|---|
| 222 | suppress_required = False
|
|---|
| 223 | for flag in self.flags:
|
|---|
| 224 | if flag['value']:
|
|---|
| 225 | if len(flag['name']) > 1: # e.g. overwrite
|
|---|
| 226 | cmd += ['--' + flag['name']]
|
|---|
| 227 | else:
|
|---|
| 228 | cmd += ['-' + flag['name']]
|
|---|
| 229 | if flag['suppress_required']:
|
|---|
| 230 | suppress_required = True
|
|---|
| 231 | for p in self.params:
|
|---|
| 232 | if p.get('value', '') == '' and p.get('required', False):
|
|---|
| 233 | if p.get('default', '') != '':
|
|---|
| 234 | cmd += ['%s=%s' % (p['name'], p['default'])]
|
|---|
| 235 | elif ignoreErrors and not suppress_required and not ignoreRequired:
|
|---|
| 236 | cmd += ['%s=%s' % (p['name'], _('<required>'))]
|
|---|
| 237 | elif p.get('value', '') == '' and p.get('default', '') != '' and not ignoreDefault:
|
|---|
| 238 | cmd += ['%s=%s' % (p['name'], p['default'])]
|
|---|
| 239 | elif p.get('value', '') != '' and \
|
|---|
| 240 | (p['value'] != p.get('default', '') or not ignoreDefault):
|
|---|
| 241 | # output only values that have been set, and different from defaults
|
|---|
| 242 | cmd += ['%s=%s' % (p['name'], p['value'])]
|
|---|
| 243 |
|
|---|
| 244 | errList = self.get_cmd_error()
|
|---|
| 245 | if ignoreErrors is False and errList:
|
|---|
| 246 | raise ValueError('\n'.join(errList))
|
|---|
| 247 |
|
|---|
| 248 | return cmd
|
|---|
| 249 |
|
|---|
| 250 | def get_options(self):
|
|---|
| 251 | """Get options
|
|---|
| 252 | """
|
|---|
| 253 | return {'flags': self.flags, 'params': self.params}
|
|---|
| 254 |
|
|---|
| 255 | def has_required(self):
|
|---|
| 256 | """Check if command has at least one required parameter
|
|---|
| 257 | """
|
|---|
| 258 | for p in self.params:
|
|---|
| 259 | if p.get('required', False):
|
|---|
| 260 | return True
|
|---|
| 261 |
|
|---|
| 262 | return False
|
|---|
| 263 |
|
|---|
| 264 | def set_param(self, aParam, aValue, element='value'):
|
|---|
| 265 | """Set param value/values.
|
|---|
| 266 | """
|
|---|
| 267 | try:
|
|---|
| 268 | param = self.get_param(aParam)
|
|---|
| 269 | except ValueError:
|
|---|
| 270 | return
|
|---|
| 271 |
|
|---|
| 272 | param[element] = aValue
|
|---|
| 273 |
|
|---|
| 274 | def set_flag(self, aFlag, aValue, element='value'):
|
|---|
| 275 | """Enable / disable flag.
|
|---|
| 276 | """
|
|---|
| 277 | try:
|
|---|
| 278 | param = self.get_flag(aFlag)
|
|---|
| 279 | except ValueError:
|
|---|
| 280 | return
|
|---|
| 281 |
|
|---|
| 282 | param[element] = aValue
|
|---|
| 283 |
|
|---|
| 284 | def set_options(self, opts):
|
|---|
| 285 | """Set flags and parameters
|
|---|
| 286 |
|
|---|
| 287 | :param opts list of flags and parameters"""
|
|---|
| 288 | for opt in opts:
|
|---|
| 289 | if opt[0] == '-': # flag
|
|---|
| 290 | self.set_flag(opt.lstrip('-'), True)
|
|---|
| 291 | else: # parameter
|
|---|
| 292 | key, value = opt.split('=', 1)
|
|---|
| 293 | self.set_param(key, value)
|
|---|
| 294 |
|
|---|
| 295 |
|
|---|
| 296 | class processTask:
|
|---|
| 297 | """A ElementTree handler for the --interface-description output,
|
|---|
| 298 | as defined in grass-interface.dtd. Extend or modify this and the
|
|---|
| 299 | DTD if the XML output of GRASS' parser is extended or modified.
|
|---|
| 300 |
|
|---|
| 301 | :param tree: root tree node
|
|---|
| 302 | :param task: grassTask instance or None
|
|---|
| 303 | :param blackList: list of flags/params to hide
|
|---|
| 304 |
|
|---|
| 305 | :return: grassTask instance
|
|---|
| 306 | """
|
|---|
| 307 | def __init__(self, tree, task=None, blackList=None):
|
|---|
| 308 | if task:
|
|---|
| 309 | self.task = task
|
|---|
| 310 | else:
|
|---|
| 311 | self.task = grassTask()
|
|---|
| 312 | if blackList:
|
|---|
| 313 | self.task.blackList = blackList
|
|---|
| 314 |
|
|---|
| 315 | self.root = tree
|
|---|
| 316 |
|
|---|
| 317 | self._process_module()
|
|---|
| 318 | self._process_params()
|
|---|
| 319 | self._process_flags()
|
|---|
| 320 | self.task.define_first()
|
|---|
| 321 |
|
|---|
| 322 | def _process_module(self):
|
|---|
| 323 | """Process module description
|
|---|
| 324 | """
|
|---|
| 325 | self.task.name = self.root.get('name', default='unknown')
|
|---|
| 326 |
|
|---|
| 327 | # keywords
|
|---|
| 328 | for keyword in self._get_node_text(self.root, 'keywords').split(','):
|
|---|
| 329 | self.task.keywords.append(keyword.strip())
|
|---|
| 330 |
|
|---|
| 331 | self.task.label = self._get_node_text(self.root, 'label')
|
|---|
| 332 | self.task.description = self._get_node_text(self.root, 'description')
|
|---|
| 333 |
|
|---|
| 334 | def _process_params(self):
|
|---|
| 335 | """Process parameters
|
|---|
| 336 | """
|
|---|
| 337 | for p in self.root.findall('parameter'):
|
|---|
| 338 | # gisprompt
|
|---|
| 339 | node_gisprompt = p.find('gisprompt')
|
|---|
| 340 | gisprompt = False
|
|---|
| 341 | age = element = prompt = None
|
|---|
| 342 | if node_gisprompt is not None:
|
|---|
| 343 | gisprompt = True
|
|---|
| 344 | age = node_gisprompt.get('age', '')
|
|---|
| 345 | element = node_gisprompt.get('element', '')
|
|---|
| 346 | prompt = node_gisprompt.get('prompt', '')
|
|---|
| 347 |
|
|---|
| 348 | # value(s)
|
|---|
| 349 | values = []
|
|---|
| 350 | values_desc = []
|
|---|
| 351 | node_values = p.find('values')
|
|---|
| 352 | if node_values is not None:
|
|---|
| 353 | for pv in node_values.findall('value'):
|
|---|
| 354 | values.append(self._get_node_text(pv, 'name'))
|
|---|
| 355 | desc = self._get_node_text(pv, 'description')
|
|---|
| 356 | if desc:
|
|---|
| 357 | values_desc.append(desc)
|
|---|
| 358 |
|
|---|
| 359 | # keydesc
|
|---|
| 360 | key_desc = []
|
|---|
| 361 | node_key_desc = p.find('keydesc')
|
|---|
| 362 | if node_key_desc is not None:
|
|---|
| 363 | for ki in node_key_desc.findall('item'):
|
|---|
| 364 | key_desc.append(ki.text)
|
|---|
| 365 |
|
|---|
| 366 | if p.get('multiple', 'no') == 'yes':
|
|---|
| 367 | multiple = True
|
|---|
| 368 | else:
|
|---|
| 369 | multiple = False
|
|---|
| 370 | if p.get('required', 'no') == 'yes':
|
|---|
| 371 | required = True
|
|---|
| 372 | else:
|
|---|
| 373 | required = False
|
|---|
| 374 |
|
|---|
| 375 | if self.task.blackList['enabled'] and \
|
|---|
| 376 | self.task.name in self.task.blackList['items'] and \
|
|---|
| 377 | p.get('name') in self.task.blackList['items'][self.task.name].get('params', []):
|
|---|
| 378 | hidden = True
|
|---|
| 379 | else:
|
|---|
| 380 | hidden = False
|
|---|
| 381 |
|
|---|
| 382 | self.task.params.append( {
|
|---|
| 383 | "name" : p.get('name'),
|
|---|
| 384 | "type" : p.get('type'),
|
|---|
| 385 | "required" : required,
|
|---|
| 386 | "multiple" : multiple,
|
|---|
| 387 | "label" : self._get_node_text(p, 'label'),
|
|---|
| 388 | "description" : self._get_node_text(p, 'description'),
|
|---|
| 389 | 'gisprompt' : gisprompt,
|
|---|
| 390 | 'age' : age,
|
|---|
| 391 | 'element' : element,
|
|---|
| 392 | 'prompt' : prompt,
|
|---|
| 393 | "guisection" : self._get_node_text(p, 'guisection'),
|
|---|
| 394 | "guidependency" : self._get_node_text(p, 'guidependency'),
|
|---|
| 395 | "default" : self._get_node_text(p, 'default'),
|
|---|
| 396 | "values" : values,
|
|---|
| 397 | "values_desc" : values_desc,
|
|---|
| 398 | "value" : '',
|
|---|
| 399 | "key_desc" : key_desc,
|
|---|
| 400 | "hidden" : hidden
|
|---|
| 401 | })
|
|---|
| 402 |
|
|---|
| 403 | def _process_flags(self):
|
|---|
| 404 | """Process flags
|
|---|
| 405 | """
|
|---|
| 406 | for p in self.root.findall('flag'):
|
|---|
| 407 | if self.task.blackList['enabled'] and \
|
|---|
| 408 | self.task.name in self.task.blackList['items'] and \
|
|---|
| 409 | p.get('name') in self.task.blackList['items'][self.task.name].get('flags', []):
|
|---|
| 410 | hidden = True
|
|---|
| 411 | else:
|
|---|
| 412 | hidden = False
|
|---|
| 413 |
|
|---|
| 414 | if p.find('suppress_required') is not None:
|
|---|
| 415 | suppress_required = True
|
|---|
| 416 | else:
|
|---|
| 417 | suppress_required = False
|
|---|
| 418 |
|
|---|
| 419 | self.task.flags.append( {
|
|---|
| 420 | "name" : p.get('name'),
|
|---|
| 421 | "label" : self._get_node_text(p, 'label'),
|
|---|
| 422 | "description" : self._get_node_text(p, 'description'),
|
|---|
| 423 | "guisection" : self._get_node_text(p, 'guisection'),
|
|---|
| 424 | "suppress_required" : suppress_required,
|
|---|
| 425 | "value" : False,
|
|---|
| 426 | "hidden" : hidden
|
|---|
| 427 | } )
|
|---|
| 428 |
|
|---|
| 429 | def _get_node_text(self, node, tag, default=''):
|
|---|
| 430 | """Get node text"""
|
|---|
| 431 | p = node.find(tag)
|
|---|
| 432 | if p is not None:
|
|---|
| 433 | res = ' '.join(p.text.split())
|
|---|
| 434 | return res
|
|---|
| 435 |
|
|---|
| 436 | return default
|
|---|
| 437 |
|
|---|
| 438 | def get_task(self):
|
|---|
| 439 | """Get grassTask instance"""
|
|---|
| 440 | return self.task
|
|---|
| 441 |
|
|---|
| 442 |
|
|---|
| 443 | def convert_xml_to_utf8(xml_text):
|
|---|
| 444 | # enc = locale.getdefaultlocale()[1]
|
|---|
| 445 |
|
|---|
| 446 | # modify: fetch encoding from the interface description text(xml)
|
|---|
| 447 | # e.g. <?xml version="1.0" encoding="GBK"?>
|
|---|
| 448 | pattern = re.compile(b'<\?xml[^>]*\Wencoding="([^"]*)"[^>]*\?>')
|
|---|
| 449 | m = re.match(pattern, xml_text)
|
|---|
| 450 | if m is None:
|
|---|
| 451 | return xml_text
|
|---|
| 452 | #
|
|---|
| 453 | enc = m.groups()[0]
|
|---|
| 454 | # for Python 3
|
|---|
| 455 | enc_decoded = enc.decode('ascii')
|
|---|
| 456 |
|
|---|
| 457 | # modify: change the encoding to "utf-8", for correct parsing
|
|---|
| 458 | xml_text_utf8 = xml_text.decode(enc_decoded).encode("utf-8")
|
|---|
| 459 | p = re.compile(b'encoding="' + enc + b'"', re.IGNORECASE)
|
|---|
| 460 | xml_text_utf8 = p.sub(b'encoding="utf-8"', xml_text_utf8)
|
|---|
| 461 |
|
|---|
| 462 | return xml_text_utf8
|
|---|
| 463 |
|
|---|
| 464 |
|
|---|
| 465 | def get_interface_description(cmd):
|
|---|
| 466 | """Returns the XML description for the GRASS cmd (force text encoding to
|
|---|
| 467 | "utf-8").
|
|---|
| 468 |
|
|---|
| 469 | The DTD must be located in $GISBASE/gui/xml/grass-interface.dtd,
|
|---|
| 470 | otherwise the parser will not succeed.
|
|---|
| 471 |
|
|---|
| 472 | :param cmd: command (name of GRASS module)
|
|---|
| 473 | """
|
|---|
| 474 | try:
|
|---|
| 475 | p = Popen([encode(cmd), b'--interface-description'], stdout=PIPE,
|
|---|
| 476 | stderr=PIPE)
|
|---|
| 477 | cmdout, cmderr = p.communicate()
|
|---|
| 478 |
|
|---|
| 479 | # TODO: do it better (?)
|
|---|
| 480 | if not cmdout and sys.platform == 'win32':
|
|---|
| 481 | # we in fact expect pure module name (without extension)
|
|---|
| 482 | # so, lets remove extension
|
|---|
| 483 | if cmd.endswith('.py'):
|
|---|
| 484 | cmd = os.path.splitext(cmd)[0]
|
|---|
| 485 |
|
|---|
| 486 | if cmd == 'd.rast3d':
|
|---|
| 487 | sys.path.insert(0, os.path.join(os.getenv('GISBASE'), 'etc',
|
|---|
| 488 | 'gui', 'scripts'))
|
|---|
| 489 |
|
|---|
| 490 | p = Popen([sys.executable, get_real_command(cmd),
|
|---|
| 491 | '--interface-description'],
|
|---|
| 492 | stdout=PIPE, stderr=PIPE)
|
|---|
| 493 | cmdout, cmderr = p.communicate()
|
|---|
| 494 |
|
|---|
| 495 | if cmd == 'd.rast3d':
|
|---|
| 496 | del sys.path[0] # remove gui/scripts from the path
|
|---|
| 497 |
|
|---|
| 498 | if p.returncode != 0:
|
|---|
| 499 | raise ScriptError(_("Unable to fetch interface description for command '<{cmd}>'."
|
|---|
| 500 | "\n\nDetails: <{det}>".format(cmd=cmd, det=decode(cmderr))))
|
|---|
| 501 |
|
|---|
| 502 | except OSError as e:
|
|---|
| 503 | raise ScriptError(_("Unable to fetch interface description for command '<{cmd}>'."
|
|---|
| 504 | "\n\nDetails: <{det}>".format(cmd=cmd, det=e)))
|
|---|
| 505 |
|
|---|
| 506 | desc = convert_xml_to_utf8(cmdout)
|
|---|
| 507 | desc = desc.replace(b'grass-interface.dtd',
|
|---|
| 508 | os.path.join(os.getenv('GISBASE'), 'gui', 'xml',
|
|---|
| 509 | 'grass-interface.dtd').encode('utf-8'))
|
|---|
| 510 | return desc
|
|---|
| 511 |
|
|---|
| 512 |
|
|---|
| 513 | def parse_interface(name, parser=processTask, blackList=None):
|
|---|
| 514 | """Parse interface of given GRASS module
|
|---|
| 515 |
|
|---|
| 516 | The *name* is either GRASS module name (of a module on path) or
|
|---|
| 517 | a full or relative path to an executable.
|
|---|
| 518 |
|
|---|
| 519 | :param str name: name of GRASS module to be parsed
|
|---|
| 520 | :param parser:
|
|---|
| 521 | :param blackList:
|
|---|
| 522 | """
|
|---|
| 523 | try:
|
|---|
| 524 | tree = etree.fromstring(get_interface_description(name))
|
|---|
| 525 | except ETREE_EXCEPTIONS as error:
|
|---|
| 526 | raise ScriptError(_("Cannot parse interface description of"
|
|---|
| 527 | "<{name}> module: {error}").format(name=name, error=error))
|
|---|
| 528 | task = parser(tree, blackList=blackList).get_task()
|
|---|
| 529 | # if name from interface is different than the originally
|
|---|
| 530 | # provided name, then the provided name is likely a full path needed
|
|---|
| 531 | # to actually run the module later
|
|---|
| 532 | # (processTask uses only the XML which does not contain the original
|
|---|
| 533 | # path used to execute the module)
|
|---|
| 534 | if task.name != name:
|
|---|
| 535 | task.path = name
|
|---|
| 536 | return task
|
|---|
| 537 |
|
|---|
| 538 |
|
|---|
| 539 | def command_info(cmd):
|
|---|
| 540 | """Returns meta information for any GRASS command as dictionary
|
|---|
| 541 | with entries for description, keywords, usage, flags, and
|
|---|
| 542 | parameters, e.g.
|
|---|
| 543 |
|
|---|
| 544 | >>> command_info('g.tempfile') # doctest: +NORMALIZE_WHITESPACE
|
|---|
| 545 | {'keywords': ['general', 'support'], 'params': [{'gisprompt': False,
|
|---|
| 546 | 'multiple': False, 'name': 'pid', 'guidependency': '', 'default': '',
|
|---|
| 547 | 'age': None, 'required': True, 'value': '', 'label': '', 'guisection': '',
|
|---|
| 548 | 'key_desc': [], 'values': [], 'values_desc': [], 'prompt': None,
|
|---|
| 549 | 'hidden': False, 'element': None, 'type': 'integer', 'description':
|
|---|
| 550 | 'Process id to use when naming the tempfile'}], 'flags': [{'description':
|
|---|
| 551 | "Dry run - don't create a file, just prints it's file name", 'value':
|
|---|
| 552 | False, 'label': '', 'guisection': '', 'suppress_required': False,
|
|---|
| 553 | 'hidden': False, 'name': 'd'}, {'description': 'Print usage summary',
|
|---|
| 554 | 'value': False, 'label': '', 'guisection': '', 'suppress_required': False,
|
|---|
| 555 | 'hidden': False, 'name': 'help'}, {'description': 'Verbose module output',
|
|---|
| 556 | 'value': False, 'label': '', 'guisection': '', 'suppress_required': False,
|
|---|
| 557 | 'hidden': False, 'name': 'verbose'}, {'description': 'Quiet module output',
|
|---|
| 558 | 'value': False, 'label': '', 'guisection': '', 'suppress_required': False,
|
|---|
| 559 | 'hidden': False, 'name': 'quiet'}], 'description': "Creates a temporary
|
|---|
| 560 | file and prints it's file name.", 'usage': 'g.tempfile pid=integer [--help]
|
|---|
| 561 | [--verbose] [--quiet]'}
|
|---|
| 562 |
|
|---|
| 563 | >>> command_info('v.buffer')
|
|---|
| 564 | ['vector', 'geometry', 'buffer']
|
|---|
| 565 |
|
|---|
| 566 | :param str cmd: the command to query
|
|---|
| 567 | """
|
|---|
| 568 | task = parse_interface(cmd)
|
|---|
| 569 | cmdinfo = {}
|
|---|
| 570 |
|
|---|
| 571 | cmdinfo['description'] = task.get_description()
|
|---|
| 572 | cmdinfo['keywords'] = task.get_keywords()
|
|---|
| 573 | cmdinfo['flags'] = flags = task.get_options()['flags']
|
|---|
| 574 | cmdinfo['params'] = params = task.get_options()['params']
|
|---|
| 575 |
|
|---|
| 576 | usage = task.get_name()
|
|---|
| 577 | flags_short = list()
|
|---|
| 578 | flags_long = list()
|
|---|
| 579 | for f in flags:
|
|---|
| 580 | fname = f.get('name', 'unknown')
|
|---|
| 581 | if len(fname) > 1:
|
|---|
| 582 | flags_long.append(fname)
|
|---|
| 583 | else:
|
|---|
| 584 | flags_short.append(fname)
|
|---|
| 585 |
|
|---|
| 586 | if len(flags_short) > 1:
|
|---|
| 587 | usage += ' [-' + ''.join(flags_short) + ']'
|
|---|
| 588 |
|
|---|
| 589 | for p in params:
|
|---|
| 590 | ptype = ','.join(p.get('key_desc', []))
|
|---|
| 591 | if not ptype:
|
|---|
| 592 | ptype = p.get('type', '')
|
|---|
| 593 | req = p.get('required', False)
|
|---|
| 594 | if not req:
|
|---|
| 595 | usage += ' ['
|
|---|
| 596 | else:
|
|---|
| 597 | usage += ' '
|
|---|
| 598 | usage += p['name'] + '=' + ptype
|
|---|
| 599 | if p.get('multiple', False):
|
|---|
| 600 | usage += '[,' + ptype + ',...]'
|
|---|
| 601 | if not req:
|
|---|
| 602 | usage += ']'
|
|---|
| 603 |
|
|---|
| 604 | for key in flags_long:
|
|---|
| 605 | usage += ' [--' + key + ']'
|
|---|
| 606 |
|
|---|
| 607 | cmdinfo['usage'] = usage
|
|---|
| 608 |
|
|---|
| 609 | return cmdinfo
|
|---|
| 610 |
|
|---|
| 611 | def cmdtuple_to_list(cmd):
|
|---|
| 612 | """Convert command tuple to list.
|
|---|
| 613 |
|
|---|
| 614 | :param tuple cmd: GRASS command to be converted
|
|---|
| 615 |
|
|---|
| 616 | :return: command in list
|
|---|
| 617 | """
|
|---|
| 618 | cmdList = []
|
|---|
| 619 | if not cmd:
|
|---|
| 620 | return cmdList
|
|---|
| 621 |
|
|---|
| 622 | cmdList.append(cmd[0])
|
|---|
| 623 |
|
|---|
| 624 | if 'flags' in cmd[1]:
|
|---|
| 625 | for flag in cmd[1]['flags']:
|
|---|
| 626 | cmdList.append('-' + flag)
|
|---|
| 627 | for flag in ('help', 'verbose', 'quiet', 'overwrite'):
|
|---|
| 628 | if flag in cmd[1] and cmd[1][flag] is True:
|
|---|
| 629 | cmdList.append('--' + flag)
|
|---|
| 630 |
|
|---|
| 631 | for k, v in cmd[1].items():
|
|---|
| 632 | if k in ('flags', 'help', 'verbose', 'quiet', 'overwrite'):
|
|---|
| 633 | continue
|
|---|
| 634 | if ' ' in v:
|
|---|
| 635 | v = '"%s"' % v
|
|---|
| 636 | cmdList.append('%s=%s' % (k, v))
|
|---|
| 637 |
|
|---|
| 638 | return cmdList
|
|---|
| 639 |
|
|---|
| 640 | def cmdlist_to_tuple(cmd):
|
|---|
| 641 | """Convert command list to tuple for run_command() and others
|
|---|
| 642 |
|
|---|
| 643 | :param list cmd: GRASS command to be converted
|
|---|
| 644 |
|
|---|
| 645 | :return: command as tuple
|
|---|
| 646 | """
|
|---|
| 647 | if len(cmd) < 1:
|
|---|
| 648 | return None
|
|---|
| 649 |
|
|---|
| 650 | dcmd = {}
|
|---|
| 651 | for item in cmd[1:]:
|
|---|
| 652 | if '=' in item: # params
|
|---|
| 653 | key, value = item.split('=', 1)
|
|---|
| 654 | dcmd[str(key)] = value.replace('"', '')
|
|---|
| 655 | elif item[:2] == '--': # long flags
|
|---|
| 656 | flag = item[2:]
|
|---|
| 657 | if flag in ('help', 'verbose', 'quiet', 'overwrite'):
|
|---|
| 658 | dcmd[str(flag)] = True
|
|---|
| 659 | elif len(item) == 2 and item[0] == '-': # -> flags
|
|---|
| 660 | if 'flags' not in dcmd:
|
|---|
| 661 | dcmd['flags'] = ''
|
|---|
| 662 | dcmd['flags'] += item[1]
|
|---|
| 663 | else: # unnamed parameter
|
|---|
| 664 | module = parse_interface(cmd[0])
|
|---|
| 665 | dcmd[module.define_first()] = item
|
|---|
| 666 |
|
|---|
| 667 | return (cmd[0], dcmd)
|
|---|
| 668 |
|
|---|
| 669 | def cmdstring_to_tuple(cmd):
|
|---|
| 670 | """Convert command string to tuple for run_command() and others
|
|---|
| 671 |
|
|---|
| 672 | :param str cmd: command to be converted
|
|---|
| 673 |
|
|---|
| 674 | :return: command as tuple
|
|---|
| 675 | """
|
|---|
| 676 | return cmdlist_to_tuple(split(cmd))
|
|---|