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_dirname(filename):
123 if not filename: 124 raise SubstitutionException("Cannot substitute $(dirname), no file/directory information available.") 125 return os.path.abspath(os.path.dirname(filename))
126
127 -def _dirname(resolved, a, args, context):
128 """ 129 process $(dirname) 130 @return: updated resolved argument 131 @rtype: str 132 @raise SubstitutionException: if no information about the current launch file is available, for example 133 if XML was passed via stdin, or this is a remote launch. 134 """ 135 return resolved.replace("$(%s)" % a, _eval_dirname(context.get('filename', None)))
136
137 -def _eval_find(pkg):
138 rp = _get_rospack() 139 return rp.get_path(pkg)
140
141 -def _find(resolved, a, args, context):
142 """ 143 process $(find PKG) 144 Resolves the path while considering the path following the command to provide backward compatible results. 145 If it is followed by a path it first tries to resolve it as an executable and than as a normal file under share. 146 Else it resolves to the source share folder of the PKG. 147 :returns: updated resolved argument, ``str`` 148 :raises: :exc:SubstitutionException: if PKG invalidly specified 149 :raises: :exc:`rospkg.ResourceNotFound` If PKG requires resource (e.g. package) that does not exist 150 """ 151 if len(args) != 1: 152 raise SubstitutionException("$(find pkg) command only accepts one argument [%s]" % a) 153 before, after = _split_command(resolved, a) 154 path, after = _separate_first_path(after) 155 resolve_without_path = before + ('$(%s)' % a) + after 156 path = _sanitize_path(path) 157 if path.startswith('/') or path.startswith('\\'): 158 path = path[1:] 159 rp = _get_rospack() 160 if path: 161 source_path_to_packages = rp.get_custom_cache('source_path_to_packages', {}) 162 res = None 163 try: 164 res = _find_executable( 165 resolve_without_path, a, [args[0], path], context, 166 source_path_to_packages=source_path_to_packages) 167 except SubstitutionException: 168 pass 169 if res is None: 170 try: 171 res = _find_resource( 172 resolve_without_path, a, [args[0], path], context, 173 source_path_to_packages=source_path_to_packages) 174 except SubstitutionException: 175 pass 176 # persist mapping of packages in rospack instance 177 if source_path_to_packages: 178 rp.set_custom_cache('source_path_to_packages', source_path_to_packages) 179 if res is not None: 180 return res 181 pkg_path = rp.get_path(args[0]) 182 if path: 183 pkg_path = os.path.join(pkg_path, path) 184 return before + pkg_path + after
185 186
187 -def _find_executable(resolved, a, args, _context, source_path_to_packages=None):
188 """ 189 process $(find-executable PKG PATH) 190 It finds the executable with the basename(PATH) in the libexec folder 191 or under the PATH relative to the package.xml file. 192 :returns: updated resolved argument, ``str`` 193 :raises: :exc:SubstitutionException: if PKG/PATH invalidly specified or executable is not found for PKG 194 """ 195 if len(args) != 2: 196 raise SubstitutionException("$(find-executable pkg path) command only accepts two argument [%s]" % a) 197 before, after = _split_command(resolved, a) 198 path = _sanitize_path(args[1]) 199 # we try to find the specific executable in libexec via catkin 200 # which will search in install/devel space 201 full_path = None 202 from catkin.find_in_workspaces import find_in_workspaces 203 paths = find_in_workspaces( 204 ['libexec'], project=args[0], first_matching_workspace_only=True, 205 # implicitly first_match_only=True 206 source_path_to_packages=source_path_to_packages) 207 if paths: 208 full_path = _get_executable_path(paths[0], os.path.basename(path)) 209 if not full_path: 210 # else we will look for the executable in the source folder of the package 211 rp = _get_rospack() 212 full_path = _get_executable_path(rp.get_path(args[0]), path) 213 if not full_path: 214 raise SubstitutionException("$(find-executable pkg path) could not find executable [%s]" % a) 215 return before + full_path + after
216 217
218 -def _find_resource(resolved, a, args, _context, source_path_to_packages=None):
219 """ 220 process $(find-resource PKG PATH) 221 Resolves the relative PATH from the share folder of the PKG either from install space, devel space or from the source folder. 222 :returns: updated resolved argument, ``str`` 223 :raises: :exc:SubstitutionException: if PKG and PATH invalidly specified or relative PATH is not found for PKG 224 """ 225 if len(args) != 2: 226 raise SubstitutionException("$(find-resource pkg path) command only accepts two argument [%s]" % a) 227 before, after = _split_command(resolved, a) 228 path = _sanitize_path(args[1]) 229 # we try to find the specific path in share via catkin 230 # which will search in install/devel space and the source folder of the package 231 from catkin.find_in_workspaces import find_in_workspaces 232 paths = find_in_workspaces( 233 ['share'], project=args[0], path=path, first_matching_workspace_only=True, 234 first_match_only=True, source_path_to_packages=source_path_to_packages) 235 if not paths: 236 raise SubstitutionException("$(find-resource pkg path) could not find path [%s]" % a) 237 return before + paths[0] + after
238 239
240 -def _split_command(resolved, command_with_args):
241 cmd = '$(%s)' % command_with_args 242 idx1 = resolved.find(cmd) 243 idx2 = idx1 + len(cmd) 244 return resolved[0:idx1], resolved[idx2:]
245 246
247 -def _separate_first_path(value):
248 idx = value.find(' ') 249 if idx < 0: 250 path, rest = value, '' 251 else: 252 path, rest = value[0:idx], value[idx:] 253 return path, rest
254 255
256 -def _sanitize_path(path):
257 path = path.replace('/', os.sep) 258 path = path.replace('\\', os.sep) 259 return path
260 261
262 -def _get_executable_path(base_path, path):
263 full_path = os.path.join(base_path, path) 264 if os.path.isfile(full_path) and os.access(full_path, os.X_OK): 265 return full_path 266 return None
267 268
269 -def _get_rospack():
270 global _rospack 271 if _rospack is None: 272 _rospack = rospkg.RosPack() 273 return _rospack
274 275
276 -def _eval_arg(name, args):
277 try: 278 return args[name] 279 except KeyError: 280 raise ArgException(name)
281
282 -def _arg(resolved, a, args, context):
283 """ 284 process $(arg) arg 285 286 :returns: updated resolved argument, ``str`` 287 :raises: :exc:`ArgException` If arg invalidly specified 288 """ 289 if len(args) == 0: 290 raise SubstitutionException("$(arg var) must specify a variable name [%s]"%(a)) 291 elif len(args) > 1: 292 raise SubstitutionException("$(arg var) may only specify one arg [%s]"%(a)) 293 294 if 'arg' not in context: 295 context['arg'] = {} 296 return resolved.replace("$(%s)" % a, _eval_arg(name=args[0], args=context['arg']))
297 298 # Create a dictionary of global symbols that will be available in the eval 299 # context. We disable all the builtins, then add back True and False, and also 300 # add true and false for convenience (because we accept those lower-case strings 301 # as boolean values in XML). 302 _eval_dict={ 303 'true': True, 'false': False, 304 'True': True, 'False': False, 305 '__builtins__': {k: __builtins__[k] for k in ['list', 'dict', 'map', 'str', 'float', 'int']}, 306 'env': _eval_env, 307 'optenv': _eval_optenv, 308 'find': _eval_find 309 } 310 # also define all math symbols and functions 311 _eval_dict.update(math.__dict__) 312
313 -class _DictWrapper(object):
314 - def __init__(self, args, functions):
315 self._args = args 316 self._functions = functions
317
318 - def __getitem__(self, key):
319 try: 320 return self._functions[key] 321 except KeyError: 322 return convert_value(self._args[key], 'auto')
323
324 -def _eval(s, context):
325 if 'anon' not in context: 326 context['anon'] = {} 327 if 'arg' not in context: 328 context['arg'] = {} 329 330 # inject correct anon context 331 def _eval_anon_context(id): return _eval_anon(id, anons=context['anon']) 332 # inject arg context 333 def _eval_arg_context(name): return convert_value(_eval_arg(name, args=context['arg']), 'auto') 334 # inject dirname context 335 def _eval_dirname_context(): return _eval_dirname(context['filename']) 336 functions = { 337 'anon': _eval_anon_context, 338 'arg': _eval_arg_context, 339 'dirname': _eval_dirname_context 340 } 341 functions.update(_eval_dict) 342 343 # ignore values containing double underscores (for safety) 344 # http://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html 345 if s.find('__') >= 0: 346 raise SubstitutionException("$(eval ...) may not contain double underscore expressions") 347 return str(eval(s, {}, _DictWrapper(context['arg'], functions)))
348
349 -def resolve_args(arg_str, context=None, resolve_anon=True, filename=None):
350 """ 351 Resolves substitution args (see wiki spec U{http://ros.org/wiki/roslaunch}). 352 353 @param arg_str: string to resolve zero or more substitution args 354 in. arg_str may be None, in which case resolve_args will 355 return None 356 @type arg_str: str 357 @param context dict: (optional) dictionary for storing results of 358 the 'anon' and 'arg' substitution args. multiple calls to 359 resolve_args should use the same context so that 'anon' 360 substitions resolve consistently. If no context is provided, a 361 new one will be created for each call. Values for the 'arg' 362 context should be stored as a dictionary in the 'arg' key. 363 @type context: dict 364 @param resolve_anon bool: If True (default), will resolve $(anon 365 foo). If false, will leave these args as-is. 366 @type resolve_anon: bool 367 368 @return str: arg_str with substitution args resolved 369 @rtype: str 370 @raise SubstitutionException: if there is an error resolving substitution args 371 """ 372 if context is None: 373 context = {} 374 if not arg_str: 375 return arg_str 376 # special handling of $(eval ...) 377 if arg_str.startswith('$(eval ') and arg_str.endswith(')'): 378 return _eval(arg_str[7:-1], context) 379 # first resolve variables like 'env' and 'arg' 380 commands = { 381 'env': _env, 382 'optenv': _optenv, 383 'dirname': _dirname, 384 'anon': _anon, 385 'arg': _arg, 386 } 387 resolved = _resolve_args(arg_str, context, resolve_anon, commands) 388 # then resolve 'find' as it requires the subsequent path to be expanded already 389 commands = { 390 'find': _find, 391 } 392 resolved = _resolve_args(resolved, context, resolve_anon, commands) 393 return resolved
394
395 -def _resolve_args(arg_str, context, resolve_anon, commands):
396 valid = ['find', 'env', 'optenv', 'dirname', 'anon', 'arg'] 397 resolved = arg_str 398 for a in _collect_args(arg_str): 399 splits = [s for s in a.split(' ') if s] 400 if not splits[0] in valid: 401 raise SubstitutionException("Unknown substitution command [%s]. Valid commands are %s"%(a, valid)) 402 command = splits[0] 403 args = splits[1:] 404 if command in commands: 405 resolved = commands[command](resolved, a, args, context) 406 return resolved
407 408 _OUT = 0 409 _DOLLAR = 1 410 _LP = 2 411 _IN = 3
412 -def _collect_args(arg_str):
413 """ 414 State-machine parser for resolve_args. Substitution args are of the form: 415 $(find package_name)/scripts/foo.py $(export some/attribute blar) non-relevant stuff 416 417 @param arg_str: argument string to parse args from 418 @type arg_str: str 419 @raise SubstitutionException: if args are invalidly specified 420 @return: list of arguments 421 @rtype: [str] 422 """ 423 buff = StringIO() 424 args = [] 425 state = _OUT 426 for c in arg_str: 427 # No escapes supported 428 if c == '$': 429 if state == _OUT: 430 state = _DOLLAR 431 elif state == _DOLLAR: 432 pass 433 else: 434 raise SubstitutionException("Dollar signs '$' cannot be inside of substitution args [%s]"%arg_str) 435 elif c == '(': 436 if state == _DOLLAR: 437 state = _LP 438 elif state != _OUT: 439 raise SubstitutionException("Invalid left parenthesis '(' in substitution args [%s]"%arg_str) 440 elif c == ')': 441 if state == _IN: 442 #save contents of collected buffer 443 args.append(buff.getvalue()) 444 buff.truncate(0) 445 buff.seek(0) 446 state = _OUT 447 else: 448 state = _OUT 449 elif state == _DOLLAR: 450 # left paren must immediately follow dollar sign to enter _IN state 451 state = _OUT 452 elif state == _LP: 453 state = _IN 454 455 if state == _IN: 456 buff.write(c) 457 return args
458