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
50 _rospack = None
51
53 """
54 Base class for exceptions in substitution_args routines
55 """
56 pass
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
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
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
176
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
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
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
206
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
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
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
233 path = path.replace('/', os.sep)
234 path = path.replace('\\', os.sep)
235 return path
236
237
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
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
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
302 if not arg_str:
303 return arg_str
304
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
313 commands = {
314 'find': _find,
315 }
316 resolved = _resolve_args(resolved, context, resolve_anon, commands)
317 return resolved
318
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
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
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
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
375 state = _OUT
376 elif state == _LP:
377 state = _IN
378
379 if state == _IN:
380 buff.write(c)
381 return args
382