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   
 50  _rospack = None 
 51   
52 -class SubstitutionException(Exception):
53 """ 54 Base class for exceptions in substitution_args routines 55 """ 56 pass
57 -class ArgException(SubstitutionException):
58 """ 59 Exception for missing $(arg) values 60 """ 61 pass
62
63 -def _env(resolved, a, args, context):
64 """ 65 process $(env) arg 66 @return: updated resolved argument 67 @rtype: str 68 @raise SubstitutionException: if arg invalidly specified 69 """ 70 if len(args) != 1: 71 raise SubstitutionException("$(env var) command only accepts one argument [%s]"%a) 72 try: 73 return resolved.replace("$(%s)"%a, os.environ[args[0]]) 74 except KeyError as e: 75 raise SubstitutionException("environment variable %s is not set"%str(e))
76
77 -def _optenv(resolved, a, args, context):
78 """ 79 process $(optenv) arg 80 @return: updated resolved argument 81 @rtype: str 82 @raise SubstitutionException: if arg invalidly specified 83 """ 84 if len(args) == 0: 85 raise SubstitutionException("$(optenv var) must specify an environment variable [%s]"%a) 86 if args[0] in os.environ: 87 return resolved.replace("$(%s)"%a, os.environ[args[0]]) 88 elif len(args) > 1: 89 return resolved.replace("$(%s)"%a, ' '.join(args[1:])) 90 else: 91 return resolved.replace("$(%s)"%a, '')
92
93 -def _anon(resolved, a, args, context):
94 """ 95 process $(anon) arg 96 @return: updated resolved argument 97 @rtype: str 98 @raise SubstitutionException: if arg invalidly specified 99 """ 100 # #1559 #1660 101 if len(args) == 0: 102 raise SubstitutionException("$(anon var) must specify a name [%s]"%a) 103 elif len(args) > 1: 104 raise SubstitutionException("$(anon var) may only specify one name [%s]"%a) 105 id = args[0] 106 if 'anon' not in context: 107 context['anon'] = {} 108 anon_context = context['anon'] 109 if id in anon_context: 110 return resolved.replace("$(%s)"%a, anon_context[id]) 111 else: 112 resolve_to = rosgraph.names.anonymous_name(id) 113 anon_context[id] = resolve_to 114 return resolved.replace("$(%s)"%a, resolve_to)
115 116
117 -def _find(resolved, a, args, context):
118 """ 119 process $(find PKG) 120 Resolves the path while considering the path following the command to provide backward compatible results. 121 If it is followed by a path it first tries to resolve it as an executable and than as a normal file under share. 122 Else it resolves to the source share folder of the PKG. 123 :returns: updated resolved argument, ``str`` 124 :raises: :exc:SubstitutionException: if PKG invalidly specified 125 :raises: :exc:`rospkg.ResourceNotFound` If PKG requires resource (e.g. package) that does not exist 126 """ 127 if len(args) != 1: 128 raise SubstitutionException("$(find pkg) command only accepts one argument [%s]" % a) 129 before, after = _split_command(resolved, a) 130 path, after = _separate_first_path(after) 131 resolve_without_path = before + ('$(%s)' % a) + after 132 path = _sanitize_path(path) 133 if path.startswith('/') or path.startswith('\\'): 134 path = path[1:] 135 rp = _get_rospack() 136 if path: 137 source_path_to_packages = rp.get_custom_cache('source_path_to_packages', {}) 138 res = None 139 try: 140 res = _find_executable( 141 resolve_without_path, a, [args[0], path], context, 142 source_path_to_packages=source_path_to_packages) 143 except SubstitutionException: 144 pass 145 if res is None: 146 try: 147 res = _find_resource( 148 resolve_without_path, a, [args[0], path], context, 149 source_path_to_packages=source_path_to_packages) 150 except SubstitutionException: 151 pass 152 # persist mapping of packages in rospack instance 153 if source_path_to_packages: 154 rp.set_custom_cache('source_path_to_packages', source_path_to_packages) 155 if res is not None: 156 return res 157 pkg_path = rp.get_path(args[0]) 158 if path: 159 pkg_path = os.path.join(pkg_path, path) 160 return before + pkg_path + after
161 162
163 -def _find_executable(resolved, a, args, _context, source_path_to_packages=None):
164 """ 165 process $(find-executable PKG PATH) 166 It finds the executable with the basename(PATH) in the libexec folder 167 or under the PATH relative to the package.xml file. 168 :returns: updated resolved argument, ``str`` 169 :raises: :exc:SubstitutionException: if PKG/PATH invalidly specified or executable is not found for PKG 170 """ 171 if len(args) != 2: 172 raise SubstitutionException("$(find-executable pkg path) command only accepts two argument [%s]" % a) 173 before, after = _split_command(resolved, a) 174 path = _sanitize_path(args[1]) 175 # we try to find the specific executable in libexec via catkin 176 # which will search in install/devel space 177 full_path = None 178 from catkin.find_in_workspaces import find_in_workspaces 179 paths = find_in_workspaces( 180 ['libexec'], project=args[0], first_matching_workspace_only=True, 181 # implicitly first_match_only=True 182 source_path_to_packages=source_path_to_packages) 183 if paths: 184 full_path = _get_executable_path(paths[0], os.path.basename(path)) 185 if not full_path: 186 # else we will look for the executable in the source folder of the package 187 rp = _get_rospack() 188 full_path = _get_executable_path(rp.get_path(args[0]), path) 189 if not full_path: 190 raise SubstitutionException("$(find-executable pkg path) could not find executable [%s]" % a) 191 return before + full_path + after
192 193
194 -def _find_resource(resolved, a, args, _context, source_path_to_packages=None):
195 """ 196 process $(find-resource PKG PATH) 197 Resolves the relative PATH from the share folder of the PKG either from install space, devel space or from the source folder. 198 :returns: updated resolved argument, ``str`` 199 :raises: :exc:SubstitutionException: if PKG and PATH invalidly specified or relative PATH is not found for PKG 200 """ 201 if len(args) != 2: 202 raise SubstitutionException("$(find-resource pkg path) command only accepts two argument [%s]" % a) 203 before, after = _split_command(resolved, a) 204 path = _sanitize_path(args[1]) 205 # we try to find the specific path in share via catkin 206 # which will search in install/devel space and the source folder of the package 207 from catkin.find_in_workspaces import find_in_workspaces 208 paths = find_in_workspaces( 209 ['share'], project=args[0], path=path, first_matching_workspace_only=True, 210 first_match_only=True, source_path_to_packages=source_path_to_packages) 211 if not paths: 212 raise SubstitutionException("$(find-resource pkg path) could not find path [%s]" % a) 213 return before + paths[0] + after
214 215
216 -def _split_command(resolved, command_with_args):
217 cmd = '$(%s)' % command_with_args 218 idx1 = resolved.find(cmd) 219 idx2 = idx1 + len(cmd) 220 return resolved[0:idx1], resolved[idx2:]
221 222
223 -def _separate_first_path(value):
224 idx = value.find(' ') 225 if idx < 0: 226 path, rest = value, '' 227 else: 228 path, rest = value[0:idx], value[idx:] 229 return path, rest
230 231
232 -def _sanitize_path(path):
233 path = path.replace('/', os.sep) 234 path = path.replace('\\', os.sep) 235 return path
236 237
238 -def _get_executable_path(base_path, path):
239 full_path = os.path.join(base_path, path) 240 if os.path.isfile(full_path) and os.access(full_path, os.X_OK): 241 return full_path 242 return None
243 244
245 -def _get_rospack():
246 global _rospack 247 if _rospack is None: 248 _rospack = rospkg.RosPack() 249 return _rospack
250 251
252 -def _arg(resolved, a, args, context):
253 """ 254 process $(arg) arg 255 256 :returns: updated resolved argument, ``str`` 257 :raises: :exc:`ArgException` If arg invalidly specified 258 """ 259 if len(args) == 0: 260 raise SubstitutionException("$(arg var) must specify an environment variable [%s]"%(a)) 261 elif len(args) > 1: 262 raise SubstitutionException("$(arg var) may only specify one arg [%s]"%(a)) 263 264 if 'arg' not in context: 265 context['arg'] = {} 266 arg_context = context['arg'] 267 268 arg_name = args[0] 269 if arg_name in arg_context: 270 arg_value = arg_context[arg_name] 271 return resolved.replace("$(%s)"%a, arg_value) 272 else: 273 raise ArgException(arg_name)
274 275
276 -def resolve_args(arg_str, context=None, resolve_anon=True):
277 """ 278 Resolves substitution args (see wiki spec U{http://ros.org/wiki/roslaunch}). 279 280 @param arg_str: string to resolve zero or more substitution args 281 in. arg_str may be None, in which case resolve_args will 282 return None 283 @type arg_str: str 284 @param context dict: (optional) dictionary for storing results of 285 the 'anon' and 'arg' substitution args. multiple calls to 286 resolve_args should use the same context so that 'anon' 287 substitions resolve consistently. If no context is provided, a 288 new one will be created for each call. Values for the 'arg' 289 context should be stored as a dictionary in the 'arg' key. 290 @type context: dict 291 @param resolve_anon bool: If True (default), will resolve $(anon 292 foo). If false, will leave these args as-is. 293 @type resolve_anon: bool 294 295 @return str: arg_str with substitution args resolved 296 @rtype: str 297 @raise SubstitutionException: if there is an error resolving substitution args 298 """ 299 if context is None: 300 context = {} 301 #parse found substitution args 302 if not arg_str: 303 return arg_str 304 # first resolve variables like 'env' and 'arg' 305 commands = { 306 'env': _env, 307 'optenv': _optenv, 308 'anon': _anon, 309 'arg': _arg, 310 } 311 resolved = _resolve_args(arg_str, context, resolve_anon, commands) 312 # than resolve 'find' as it requires the subsequent path to be expanded already 313 commands = { 314 'find': _find, 315 } 316 resolved = _resolve_args(resolved, context, resolve_anon, commands) 317 return resolved
318
319 -def _resolve_args(arg_str, context, resolve_anon, commands):
320 valid = ['find', 'env', 'optenv', 'anon', 'arg'] 321 resolved = arg_str 322 for a in _collect_args(arg_str): 323 splits = [s for s in a.split(' ') if s] 324 if not splits[0] in valid: 325 raise SubstitutionException("Unknown substitution command [%s]. Valid commands are %s"%(a, valid)) 326 command = splits[0] 327 args = splits[1:] 328 if command in commands: 329 resolved = commands[command](resolved, a, args, context) 330 return resolved
331 332 _OUT = 0 333 _DOLLAR = 1 334 _LP = 2 335 _IN = 3
336 -def _collect_args(arg_str):
337 """ 338 State-machine parser for resolve_args. Substitution args are of the form: 339 $(find package_name)/scripts/foo.py $(export some/attribute blar) non-relevant stuff 340 341 @param arg_str: argument string to parse args from 342 @type arg_str: str 343 @raise SubstitutionException: if args are invalidly specified 344 @return: list of arguments 345 @rtype: [str] 346 """ 347 buff = StringIO() 348 args = [] 349 state = _OUT 350 for c in arg_str: 351 # No escapes supported 352 if c == '$': 353 if state == _OUT: 354 state = _DOLLAR 355 elif state == _DOLLAR: 356 pass 357 else: 358 raise SubstitutionException("Dollar signs '$' cannot be inside of substitution args [%s]"%arg_str) 359 elif c == '(': 360 if state == _DOLLAR: 361 state = _LP 362 elif state != _OUT: 363 raise SubstitutionException("Invalid left parenthesis '(' in substitution args [%s]"%arg_str) 364 elif c == ')': 365 if state == _IN: 366 #save contents of collected buffer 367 args.append(buff.getvalue()) 368 buff.truncate(0) 369 buff.seek(0) 370 state = _OUT 371 else: 372 state = _OUT 373 elif state == _DOLLAR: 374 # left paren must immediately follow dollar sign to enter _IN state 375 state = _OUT 376 elif state == _LP: 377 state = _IN 378 379 if state == _IN: 380 buff.write(c) 381 return args
382