Package roslaunch :: Module substitution_args
[frames] | no frames]

Source Code for Module roslaunch.substitution_args

  1  # Software License Agreement (BSD License) 
  2  # 
  3  # Copyright (c) 2008, Willow Garage, Inc. 
  4  # All rights reserved. 
  5  # 
  6  # Redistribution and use in source and binary forms, with or without 
  7  # modification, are permitted provided that the following conditions 
  8  # are met: 
  9  # 
 10  #  * Redistributions of source code must retain the above copyright 
 11  #    notice, this list of conditions and the following disclaimer. 
 12  #  * Redistributions in binary form must reproduce the above 
 13  #    copyright notice, this list of conditions and the following 
 14  #    disclaimer in the documentation and/or other materials provided 
 15  #    with the distribution. 
 16  #  * Neither the name of Willow Garage, Inc. nor the names of its 
 17  #    contributors may be used to endorse or promote products derived 
 18  #    from this software without specific prior written permission. 
 19  # 
 20  # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
 21  # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
 22  # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 
 23  # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 
 24  # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 25  # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
 26  # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
 27  # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
 28  # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 
 29  # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
 30  # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
 31  # POSSIBILITY OF SUCH DAMAGE. 
 32  # 
 33  # Revision $Id: substitution_args.py 15178 2011-10-10 21:22:53Z kwc $ 
 34   
 35  """ 
 36  Library for processing XML substitution args. This is currently used 
 37  by roslaunch and xacro, but it is not yet a top-level ROS feature. 
 38  """ 
 39   
 40  import os 
 41   
 42  try: 
 43      from cStringIO import StringIO # Python 2.x 
 44  except ImportError: 
 45      from io import StringIO # Python 3.x 
 46   
 47  import rosgraph.names 
 48  import rospkg 
 49  from roslaunch.loader import convert_value 
 50  import math 
 51   
 52  _rospack = None 
 53   
54 -class SubstitutionException(Exception):
55 """ 56 Base class for exceptions in substitution_args routines 57 """ 58 pass
59 -class ArgException(SubstitutionException):
60 """ 61 Exception for missing $(arg) values 62 """ 63 pass
64
65 -def _eval_env(name):
66 try: 67 return os.environ[name] 68 except KeyError as e: 69 raise SubstitutionException("environment variable %s is not set" % str(e))
70
71 -def _env(resolved, a, args, context):
72 """ 73 process $(env) arg 74 @return: updated resolved argument 75 @rtype: str 76 @raise SubstitutionException: if arg invalidly specified 77 """ 78 if len(args) != 1: 79 raise SubstitutionException("$(env var) command only accepts one argument [%s]"%a) 80 return resolved.replace("$(%s)" % a, _eval_env(args[0]))
81
82 -def _eval_optenv(name, default=''):
83 if name in os.environ: 84 return os.environ[name] 85 return default
86
87 -def _optenv(resolved, a, args, context):
88 """ 89 process $(optenv) arg 90 @return: updated resolved argument 91 @rtype: str 92 @raise SubstitutionException: if arg invalidly specified 93 """ 94 if len(args) == 0: 95 raise SubstitutionException("$(optenv var) must specify an environment variable [%s]"%a) 96 return resolved.replace("$(%s)" % a, _eval_optenv(args[0], default=' '.join(args[1:])))
97
98 -def _eval_anon(id, anons):
99 if id in anons: 100 return anons[id] 101 resolve_to = rosgraph.names.anonymous_name(id) 102 anons[id] = resolve_to 103 return resolve_to
104
105 -def _anon(resolved, a, args, context):
106 """ 107 process $(anon) arg 108 @return: updated resolved argument 109 @rtype: str 110 @raise SubstitutionException: if arg invalidly specified 111 """ 112 # #1559 #1660 113 if len(args) == 0: 114 raise SubstitutionException("$(anon var) must specify a name [%s]"%a) 115 elif len(args) > 1: 116 raise SubstitutionException("$(anon var) may only specify one name [%s]"%a) 117 if 'anon' not in context: 118 context['anon'] = {} 119 anon_context = context['anon'] 120 return resolved.replace("$(%s)" % a, _eval_anon(id=args[0], anons=anon_context))
121
122 -def _eval_find(pkg):
123 rp = _get_rospack() 124 return rp.get_path(pkg)
125
126 -def _find(resolved, a, args, context):
127 """ 128 process $(find PKG) 129 Resolves the path while considering the path following the command to provide backward compatible results. 130 If it is followed by a path it first tries to resolve it as an executable and than as a normal file under share. 131 Else it resolves to the source share folder of the PKG. 132 :returns: updated resolved argument, ``str`` 133 :raises: :exc:SubstitutionException: if PKG invalidly specified 134 :raises: :exc:`rospkg.ResourceNotFound` If PKG requires resource (e.g. package) that does not exist 135 """ 136 if len(args) != 1: 137 raise SubstitutionException("$(find pkg) command only accepts one argument [%s]" % a) 138 before, after = _split_command(resolved, a) 139 path, after = _separate_first_path(after) 140 resolve_without_path = before + ('$(%s)' % a) + after 141 path = _sanitize_path(path) 142 if path.startswith('/') or path.startswith('\\'): 143 path = path[1:] 144 rp = _get_rospack() 145 if path: 146 source_path_to_packages = rp.get_custom_cache('source_path_to_packages', {}) 147 res = None 148 try: 149 res = _find_executable( 150 resolve_without_path, a, [args[0], path], context, 151 source_path_to_packages=source_path_to_packages) 152 except SubstitutionException: 153 pass 154 if res is None: 155 try: 156 res = _find_resource( 157 resolve_without_path, a, [args[0], path], context, 158 source_path_to_packages=source_path_to_packages) 159 except SubstitutionException: 160 pass 161 # persist mapping of packages in rospack instance 162 if source_path_to_packages: 163 rp.set_custom_cache('source_path_to_packages', source_path_to_packages) 164 if res is not None: 165 return res 166 pkg_path = rp.get_path(args[0]) 167 if path: 168 pkg_path = os.path.join(pkg_path, path) 169 return before + pkg_path + after
170 171
172 -def _find_executable(resolved, a, args, _context, source_path_to_packages=None):
173 """ 174 process $(find-executable PKG PATH) 175 It finds the executable with the basename(PATH) in the libexec folder 176 or under the PATH relative to the package.xml file. 177 :returns: updated resolved argument, ``str`` 178 :raises: :exc:SubstitutionException: if PKG/PATH invalidly specified or executable is not found for PKG 179 """ 180 if len(args) != 2: 181 raise SubstitutionException("$(find-executable pkg path) command only accepts two argument [%s]" % a) 182 before, after = _split_command(resolved, a) 183 path = _sanitize_path(args[1]) 184 # we try to find the specific executable in libexec via catkin 185 # which will search in install/devel space 186 full_path = None 187 from catkin.find_in_workspaces import find_in_workspaces 188 paths = find_in_workspaces( 189 ['libexec'], project=args[0], first_matching_workspace_only=True, 190 # implicitly first_match_only=True 191 source_path_to_packages=source_path_to_packages) 192 if paths: 193 full_path = _get_executable_path(paths[0], os.path.basename(path)) 194 if not full_path: 195 # else we will look for the executable in the source folder of the package 196 rp = _get_rospack() 197 full_path = _get_executable_path(rp.get_path(args[0]), path) 198 if not full_path: 199 raise SubstitutionException("$(find-executable pkg path) could not find executable [%s]" % a) 200 return before + full_path + after
201 202
203 -def _find_resource(resolved, a, args, _context, source_path_to_packages=None):
204 """ 205 process $(find-resource PKG PATH) 206 Resolves the relative PATH from the share folder of the PKG either from install space, devel space or from the source folder. 207 :returns: updated resolved argument, ``str`` 208 :raises: :exc:SubstitutionException: if PKG and PATH invalidly specified or relative PATH is not found for PKG 209 """ 210 if len(args) != 2: 211 raise SubstitutionException("$(find-resource pkg path) command only accepts two argument [%s]" % a) 212 before, after = _split_command(resolved, a) 213 path = _sanitize_path(args[1]) 214 # we try to find the specific path in share via catkin 215 # which will search in install/devel space and the source folder of the package 216 from catkin.find_in_workspaces import find_in_workspaces 217 paths = find_in_workspaces( 218 ['share'], project=args[0], path=path, first_matching_workspace_only=True, 219 first_match_only=True, source_path_to_packages=source_path_to_packages) 220 if not paths: 221 raise SubstitutionException("$(find-resource pkg path) could not find path [%s]" % a) 222 return before + paths[0] + after
223 224
225 -def _split_command(resolved, command_with_args):
226 cmd = '$(%s)' % command_with_args 227 idx1 = resolved.find(cmd) 228 idx2 = idx1 + len(cmd) 229 return resolved[0:idx1], resolved[idx2:]
230 231
232 -def _separate_first_path(value):
233 idx = value.find(' ') 234 if idx < 0: 235 path, rest = value, '' 236 else: 237 path, rest = value[0:idx], value[idx:] 238 return path, rest
239 240
241 -def _sanitize_path(path):
242 path = path.replace('/', os.sep) 243 path = path.replace('\\', os.sep) 244 return path
245 246
247 -def _get_executable_path(base_path, path):
248 full_path = os.path.join(base_path, path) 249 if os.path.isfile(full_path) and os.access(full_path, os.X_OK): 250 return full_path 251 return None
252 253
254 -def _get_rospack():
255 global _rospack 256 if _rospack is None: 257 _rospack = rospkg.RosPack() 258 return _rospack
259 260
261 -def _eval_arg(name, args):
262 try: 263 return args[name] 264 except KeyError: 265 raise ArgException(name)
266
267 -def _arg(resolved, a, args, context):
268 """ 269 process $(arg) arg 270 271 :returns: updated resolved argument, ``str`` 272 :raises: :exc:`ArgException` If arg invalidly specified 273 """ 274 if len(args) == 0: 275 raise SubstitutionException("$(arg var) must specify a variable name [%s]"%(a)) 276 elif len(args) > 1: 277 raise SubstitutionException("$(arg var) may only specify one arg [%s]"%(a)) 278 279 if 'arg' not in context: 280 context['arg'] = {} 281 return resolved.replace("$(%s)" % a, _eval_arg(name=args[0], args=context['arg']))
282 283 # Create a dictionary of global symbols that will be available in the eval 284 # context. We disable all the builtins, then add back True and False, and also 285 # add true and false for convenience (because we accept those lower-case strings 286 # as boolean values in XML). 287 _eval_dict={ 288 'true': True, 'false': False, 289 'True': True, 'False': False, 290 '__builtins__': {k: __builtins__[k] for k in ['list', 'dict', 'map', 'str', 'float', 'int']}, 291 'env': _eval_env, 292 'optenv': _eval_optenv, 293 'find': _eval_find 294 } 295 # also define all math symbols and functions 296 _eval_dict.update(math.__dict__) 297
298 -class _DictWrapper(object):
299 - def __init__(self, args, functions):
300 self._args = args 301 self._functions = functions
302
303 - def __getitem__(self, key):
304 try: 305 return self._functions[key] 306 except KeyError: 307 return convert_value(self._args[key], 'auto')
308
309 -def _eval(s, context):
310 if 'anon' not in context: 311 context['anon'] = {} 312 if 'arg' not in context: 313 context['arg'] = {} 314 315 # inject correct anon context 316 def _eval_anon_context(id): return _eval_anon(id, anons=context['anon']) 317 # inject arg context 318 def _eval_arg_context(name): return convert_value(_eval_arg(name, args=context['arg']), 'auto') 319 functions = dict(anon=_eval_anon_context, arg=_eval_arg_context) 320 functions.update(_eval_dict) 321 322 # ignore values containing double underscores (for safety) 323 # http://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html 324 if s.find('__') >= 0: 325 raise SubstitutionException("$(eval ...) may not contain double underscore expressions") 326 return str(eval(s, {}, _DictWrapper(context['arg'], functions)))
327
328 -def resolve_args(arg_str, context=None, resolve_anon=True):
329 """ 330 Resolves substitution args (see wiki spec U{http://ros.org/wiki/roslaunch}). 331 332 @param arg_str: string to resolve zero or more substitution args 333 in. arg_str may be None, in which case resolve_args will 334 return None 335 @type arg_str: str 336 @param context dict: (optional) dictionary for storing results of 337 the 'anon' and 'arg' substitution args. multiple calls to 338 resolve_args should use the same context so that 'anon' 339 substitions resolve consistently. If no context is provided, a 340 new one will be created for each call. Values for the 'arg' 341 context should be stored as a dictionary in the 'arg' key. 342 @type context: dict 343 @param resolve_anon bool: If True (default), will resolve $(anon 344 foo). If false, will leave these args as-is. 345 @type resolve_anon: bool 346 347 @return str: arg_str with substitution args resolved 348 @rtype: str 349 @raise SubstitutionException: if there is an error resolving substitution args 350 """ 351 if context is None: 352 context = {} 353 if not arg_str: 354 return arg_str 355 # special handling of $(eval ...) 356 if arg_str.startswith('$(eval ') and arg_str.endswith(')'): 357 return _eval(arg_str[7:-1], context) 358 # first resolve variables like 'env' and 'arg' 359 commands = { 360 'env': _env, 361 'optenv': _optenv, 362 'anon': _anon, 363 'arg': _arg, 364 } 365 resolved = _resolve_args(arg_str, context, resolve_anon, commands) 366 # then resolve 'find' as it requires the subsequent path to be expanded already 367 commands = { 368 'find': _find, 369 } 370 resolved = _resolve_args(resolved, context, resolve_anon, commands) 371 return resolved
372
373 -def _resolve_args(arg_str, context, resolve_anon, commands):
374 valid = ['find', 'env', 'optenv', 'anon', 'arg'] 375 resolved = arg_str 376 for a in _collect_args(arg_str): 377 splits = [s for s in a.split(' ') if s] 378 if not splits[0] in valid: 379 raise SubstitutionException("Unknown substitution command [%s]. Valid commands are %s"%(a, valid)) 380 command = splits[0] 381 args = splits[1:] 382 if command in commands: 383 resolved = commands[command](resolved, a, args, context) 384 return resolved
385 386 _OUT = 0 387 _DOLLAR = 1 388 _LP = 2 389 _IN = 3
390 -def _collect_args(arg_str):
391 """ 392 State-machine parser for resolve_args. Substitution args are of the form: 393 $(find package_name)/scripts/foo.py $(export some/attribute blar) non-relevant stuff 394 395 @param arg_str: argument string to parse args from 396 @type arg_str: str 397 @raise SubstitutionException: if args are invalidly specified 398 @return: list of arguments 399 @rtype: [str] 400 """ 401 buff = StringIO() 402 args = [] 403 state = _OUT 404 for c in arg_str: 405 # No escapes supported 406 if c == '$': 407 if state == _OUT: 408 state = _DOLLAR 409 elif state == _DOLLAR: 410 pass 411 else: 412 raise SubstitutionException("Dollar signs '$' cannot be inside of substitution args [%s]"%arg_str) 413 elif c == '(': 414 if state == _DOLLAR: 415 state = _LP 416 elif state != _OUT: 417 raise SubstitutionException("Invalid left parenthesis '(' in substitution args [%s]"%arg_str) 418 elif c == ')': 419 if state == _IN: 420 #save contents of collected buffer 421 args.append(buff.getvalue()) 422 buff.truncate(0) 423 buff.seek(0) 424 state = _OUT 425 else: 426 state = _OUT 427 elif state == _DOLLAR: 428 # left paren must immediately follow dollar sign to enter _IN state 429 state = _OUT 430 elif state == _LP: 431 state = _IN 432 433 if state == _IN: 434 buff.write(c) 435 return args
436