1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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
44 except ImportError:
45 from io import StringIO
46
47 import rosgraph.names
48 import rospkg
49 from roslaunch.loader import convert_value
50 import math
51
52 _rospack = None
53
55 """
56 Base class for exceptions in substitution_args routines
57 """
58 pass
60 """
61 Exception for missing $(arg) values
62 """
63 pass
64
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
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
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
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
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
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
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
200
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
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
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
230
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
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
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
257 path = path.replace('/', os.sep)
258 path = path.replace('\\', os.sep)
259 return path
260
261
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
274
275
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
299
300
301
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
311 _eval_dict.update(math.__dict__)
312
315 self._args = args
316 self._functions = functions
317
319 try:
320 return self._functions[key]
321 except KeyError:
322 return convert_value(self._args[key], 'auto')
323
325 if 'anon' not in context:
326 context['anon'] = {}
327 if 'arg' not in context:
328 context['arg'] = {}
329
330
331 def _eval_anon_context(id): return _eval_anon(id, anons=context['anon'])
332
333 def _eval_arg_context(name): return convert_value(_eval_arg(name, args=context['arg']), 'auto')
334
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
344
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
377 if arg_str.startswith('$(eval ') and arg_str.endswith(')'):
378 return _eval(arg_str[7:-1], context)
379
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
389 commands = {
390 'find': _find,
391 }
392 resolved = _resolve_args(resolved, context, resolve_anon, commands)
393 return resolved
394
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
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
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
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
451 state = _OUT
452 elif state == _LP:
453 state = _IN
454
455 if state == _IN:
456 buff.write(c)
457 return args
458