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
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
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
185
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
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
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
215
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
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
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
242 path = path.replace('/', os.sep)
243 path = path.replace('\\', os.sep)
244 return path
245
246
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
259
260
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
284
285
286
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
296 _eval_dict.update(math.__dict__)
297
300 self._args = args
301 self._functions = functions
302
304 try:
305 return self._functions[key]
306 except KeyError:
307 return convert_value(self._args[key], 'auto')
308
310 if 'anon' not in context:
311 context['anon'] = {}
312 if 'arg' not in context:
313 context['arg'] = {}
314
315
316 def _eval_anon_context(id): return _eval_anon(id, anons=context['anon'])
317
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
323
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
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
356 if arg_str.startswith('$(eval ') and arg_str.endswith(')'):
357 return _eval(arg_str[7:-1], context)
358
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
367 commands = {
368 'find': _find,
369 }
370 resolved = _resolve_args(resolved, context, resolve_anon, commands)
371 return resolved
372
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
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
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
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
429 state = _OUT
430 elif state == _LP:
431 state = _IN
432
433 if state == _IN:
434 buff.write(c)
435 return args
436