5 ------------------------------------------------------------------------------
6 This file is part of grepros - grep for ROS bag files and live topics.
7 Released under the BSD License.
12 ------------------------------------------------------------------------------
30 In highlighted results, message field values that match search criteria are modified
31 to wrap the matching parts in {@link grepros.common.MatchMarkers MatchMarkers} tags,
32 with numeric field values converted to strings beforehand.
36 GrepMessage = collections.namedtuple(
"BagMessage",
"topic message timestamp match index")
39 ANY_MATCHES = [((), re.compile(
"(.*)", re.DOTALL)), (), re.compile(
"(.?)", re.DOTALL)]
42 DEFAULT_ARGS = dict(PATTERN=(), CASE=
False, FIXED_STRING=
False, INVERT=
False, HIGHLIGHT=
False,
43 NTH_MATCH=1, BEFORE=0, AFTER=0, CONTEXT=0, MAX_COUNT=0,
44 MAX_PER_TOPIC=0, MAX_TOPICS=0, SELECT_FIELD=(), NOSELECT_FIELD=(),
50 @param args arguments as namespace or dictionary, case-insensitive
51 @param args.pattern pattern(s) to find in message field values
52 @param args.fixed_string pattern contains ordinary strings, not regular expressions
53 @param args.case use case-sensitive matching in pattern
54 @param args.invert select messages not matching pattern
55 @param args.highlight highlight matched values
56 @param args.before number of messages of leading context to emit before match
57 @param args.after number of messages of trailing context to emit after match
58 @param args.context number of messages of leading and trailing context to emit
59 around match, overrides args.before and args.after
60 @param args.max_count number of matched messages to emit (per file if bag input)
61 @param args.max_per_topic number of matched messages to emit from each topic
62 @param args.max_topics number of topics to emit matches from
63 @param args.nth_match emit every Nth match in topic
64 @param args.select_field message fields to use in matching if not all
65 @param args.noselect_field message fields to skip in matching
66 @param args.match_wrapper string to wrap around matched values in find() and match(),
67 both sides if one value, start and end if more than one,
68 or no wrapping if zero values (default "**")
69 @param kwargs any and all arguments as keyword overrides, case-insensitive
72 Additional arguments when using match() or find(grepros.api.Bag):
74 @param args.topic ROS topics to read if not all
75 @param args.type ROS message types to read if not all
76 @param args.skip_topic ROS topics to skip
77 @param args.skip_type ROS message types to skip
78 @param args.start_time earliest timestamp of messages to read
79 @param args.end_time latest timestamp of messages to read
80 @param args.start_index message index within topic to start from
81 @param args.end_index message index within topic to stop at
82 @param args.unique emit messages that are unique in topic
83 @param args.nth_message read every Nth message in topic
84 @param args.nth_interval minimum time interval between messages in topic
85 @param args.condition Python expressions that must evaluate as true
86 for message to be processable, see ConditionMixin
87 @param args.progress whether to print progress bar
88 @param args.stop_on_error stop execution on any error like unknown message type
93 self.
_messages = collections.defaultdict(collections.OrderedDict)
95 self.
_stamps = collections.defaultdict(collections.OrderedDict)
97 self.
_counts = collections.defaultdict(collections.Counter)
99 self.
_statuses = collections.defaultdict(collections.OrderedDict)
111 self.
args = common.ensure_namespace(args, Scanner.DEFAULT_ARGS, **kwargs)
112 if self.
args.CONTEXT: self.
args.BEFORE = self.
args.AFTER = self.
args.CONTEXT
116 def find(self, source, highlight=None):
118 Yields matched and context messages from source.
120 @param source inputs.Source or api.Bag instance
121 @param highlight whether to highlight matched values in message fields,
122 defaults to flag from constructor
123 @return GrepMessage namedtuples of
124 (topic, message, timestamp, match, index in topic),
125 where match is matched optionally highlighted message
126 or `None` if yielding a context message
128 if isinstance(source,
api.Bag):
130 self.
_prepare(source, highlight=highlight)
131 for topic, msg, stamp, matched, index
in self.
_generate():
132 yield self.
GrepMessage(topic, msg, stamp, matched, index)
135 def match(self, topic, msg, stamp, highlight=None):
137 Returns matched message if message matches search filters.
139 @param topic topic name
140 @param msg ROS message
141 @param stamp message ROS timestamp
142 @param highlight whether to highlight matched values in message fields,
143 defaults to flag from constructor
144 @return original or highlighted message on match else `None`
151 self.
source.push(topic, msg, stamp)
152 item = self.
source.read_queue()
155 topickey = api.TypeMeta.make(msg, topic).topickey
159 self.
source.notify(matched)
160 if matched
and not self.
_counts[topickey][
True] % (self.
args.NTH_MATCH
or 1):
162 self.
_counts[topickey][
True] += 1
166 self.
_counts[topickey][
True] += 1
168 self.
source.mark_queue(topic, msg, stamp)
174 Greps messages yielded from source and emits matched content to sink.
176 @param source inputs.Source or api.Bag instance
177 @param sink outputs.Sink instance
178 @return count matched
180 if isinstance(source,
api.Bag):
182 self.
_prepare(source, sink, highlight=self.
args.HIGHLIGHT)
184 for topic, msg, stamp, matched, index
in self.
_generate():
186 sink.emit(topic, msg, stamp, matched, index)
187 total_matched += bool(matched)
192 """Context manager entry, does nothing, returns self."""
196 def __exit__(self, exc_type, exc_value, traceback):
197 """Context manager exit, does nothing."""
203 Yields matched and context messages from source.
205 @return tuples of (topic, msg, stamp, matched optionally highlighted msg, index in topic)
207 batch_matched, batch =
False,
None
208 for topic, msg, stamp
in self.
source.read():
209 if batch != self.
source.get_batch():
210 batch, batch_matched = self.
source.get_batch(),
False
214 topickey = api.TypeMeta.make(msg, topic).topickey
218 self.
source.notify(matched)
219 if matched
and not self.
_counts[topickey][
True] % (self.
args.NTH_MATCH
or 1):
221 self.
_counts[topickey][
True] += 1
223 yield (topic, msg, stamp, matched, self.
_counts[topickey][
None])
226 self.
_counts[topickey][
True] += 1
227 elif self.
args.AFTER \
230 batch_matched = batch_matched
or bool(matched)
240 Returns whether processing current message in topic is acceptable:
241 that topic or total maximum count has not been reached,
242 and current message in topic is in configured range, if any.
244 topickey = api.TypeMeta.make(msg, topic).topickey
245 if self.
args.MAX_COUNT \
246 and sum(x[
True]
for x
in self.
_counts.values()) >= self.
args.MAX_COUNT:
248 if self.
args.MAX_PER_TOPIC
and self.
_counts[topickey][
True] >= self.
args.MAX_PER_TOPIC:
250 if self.
args.MAX_TOPICS:
251 topics_matched = [k
for k, vv
in self.
_counts.items()
if vv[
True]]
252 if topickey
not in topics_matched
and len(topics_matched) >= self.
args.MAX_TOPICS:
255 and not self.
source.is_processable(topic, msg, stamp, self.
_counts[topickey][
None]):
261 """Yields before/after context for latest match."""
262 count = self.
args.BEFORE + 1
if before
else self.
args.AFTER
263 candidates = list(self.
_statuses[topickey])[-count:]
264 current_index = self.
_counts[topickey][
None]
265 for i, msgid
in enumerate(candidates)
if count
else ():
266 if self.
_statuses[topickey][msgid]
is None:
267 idx = current_index + i - (len(candidates) - 1
if before
else 1)
269 self.
_counts[topickey][
False] += 1
270 yield topickey[0], msg, stamp,
None, idx
275 """Clears local structures."""
281 def _prepare(self, source, sink=None, highlight=None):
282 """Clears local structures, binds and registers source and sink, if any."""
285 source.bind(sink), sink
and sink.bind(source)
286 source.preprocess =
False
291 """Drops history older than context window."""
292 WINDOW = max(self.
args.BEFORE, self.
args.AFTER) + 1
294 while len(dct[topickey]) > WINDOW:
295 msgid = next(iter(dct[topickey]))
296 value = dct[topickey].pop(msgid)
297 dct
is self.
_messages and api.TypeMeta.discard(value)
301 """Parses pattern arguments into re.Patterns."""
302 NOBRUTE_SIGILS =
r"\A",
r"\Z",
"?("
303 BRUTE, FLAGS =
not self.
args.INVERT, re.DOTALL | (0
if self.
args.CASE
else re.I)
307 for v
in self.
args.PATTERN:
308 split = v.find(
"=", 1, -1)
309 v, path = (v[split + 1:], v[:split])
if split > 0
else (v, ())
311 v =
"|^$" if v
in (
"''",
'""')
else (re.escape(v)
if self.
args.FIXED_STRING
else v)
312 path = re.compile(
r"(^|\.)%s($|\.)" %
".*".join(map(re.escape, path.split(
"*")))) \
314 contents.append((path, re.compile(
"(%s)" % v, FLAGS)))
315 if BRUTE
and (self.
args.FIXED_STRING
or not any(x
in v
for x
in NOBRUTE_SIGILS)):
317 if not self.
args.PATTERN:
321 selects, noselects = self.
args.SELECT_FIELD, self.
args.NOSELECT_FIELD
322 for key, vals
in [(
"select", selects), (
"noselect", noselects)]:
323 self.
_patterns[key] = [(tuple(v.split(
".")), common.wildcard_to_regex(v))
for v
in vals]
327 """Registers message with local structures."""
328 self.
_counts[topickey][
None] += 1
330 self.
_stamps [topickey][msgid] = stamp
335 """Sets highlight and passthrough flags from current settings."""
336 self.
_highlight = bool(highlight
if highlight
is not None else
337 False if self.
sink and not self.
sink.is_highlighting()
else
340 and not self.
_patterns[
"noselect"]
and not self.
args.INVERT \
345 """Returns whether max match count has been reached (and message after-context emitted)."""
346 result, is_maxed =
False,
False
347 if self.
args.MAX_COUNT:
348 is_maxed = sum(vv[
True]
for vv
in self.
_counts.values()) >= self.
args.MAX_COUNT
349 if not is_maxed
and self.
args.MAX_PER_TOPIC:
350 count_required = self.
args.MAX_TOPICS
or len(self.
source.topics)
351 count_maxed = sum(vv[
True] >= self.
args.MAX_PER_TOPIC
352 or vv[
None] >= (self.
source.topics.get(k)
or 0)
353 for k, vv
in self.
_counts.items())
354 is_maxed = (count_maxed >= count_required)
356 result =
not self.
args.AFTER
or \
363 """Returns whether given status exists in recent message window."""
364 if not length
or full
and len(self.
_statuses[topickey]) < length:
366 return status
in list(self.
_statuses[topickey].values())[-length:]
371 Returns transformed message if all patterns find a match in message, else None.
373 Matching field values are converted to strings and surrounded by markers.
374 Returns original message if any-match and sink does not require highlighting.
377 def wrap_matches(v, top, is_collection=False):
378 """Returns string with matching parts wrapped in marker tags; updates `matched`."""
381 v1 = v2 = v[1:-1]
if is_collection
and v !=
"[]" else v
382 topstr =
".".join(top)
383 for i, (path, p)
in enumerate(self.
_patterns[
"content"]):
384 if path
and not path.search(topstr):
continue
385 matches = [next(p.finditer(v1),
None)]
if self.
args.INVERT
else list(p.finditer(v1))
387 matchspans = common.merge_spans([x.span()
for x
in matches
if x], join_blanks=
True)
388 matchspans = [(a, b
if a != b
else len(v1))
for a, b
in matchspans]
391 spans.extend(matchspans)
393 spans = common.merge_spans(spans)
if not self.
args.INVERT
else \
394 []
if spans
else [(0, len(v1))]
if v1
or not is_collection
else []
395 for a, b
in reversed(spans):
396 v2 = v2[:a] + WRAPS[0] + v2[a:b] + WRAPS[1] + v2[b:]
397 return "[%s]" % v2
if is_collection
and v !=
"[]" else v2
399 def process_message(obj, top=()):
400 """Recursively converts field values to pattern-matched strings; updates `matched`."""
401 LISTIFIABLES = (bytes, tuple)
if six.PY3
else (tuple, )
403 fieldmap = fieldmap0 = api.get_message_fields(obj)
405 fieldmap = api.filter_fields(fieldmap, top, include=selects, exclude=noselects)
406 for k, t
in fieldmap.items()
if fieldmap != obj
else ():
407 v, path = api.get_message_value(obj, k, t), top + (k, )
408 is_collection = isinstance(v, (list, tuple))
409 if api.is_ros_message(v):
410 process_message(v, path)
411 elif v
and is_collection
and api.scalar(t)
not in api.ROS_NUMERIC_TYPES:
412 api.set_message_value(obj, k, [process_message(x, path)
for x
in v])
414 v1 = str(list(v)
if isinstance(v, LISTIFIABLES)
else v)
415 v2 = wrap_matches(v1, path, is_collection)
416 if len(v1) != len(v2):
417 api.set_message_value(obj, k, v2)
418 if not api.is_ros_message(obj):
419 v1 = str(list(obj)
if isinstance(obj, LISTIFIABLES)
else obj)
420 v2 = wrap_matches(v1, top)
421 obj = v2
if len(v1) != len(v2)
else obj
422 if not top
and not matched
and not selects
and not fieldmap0
and not self.
args.INVERT \
424 matched.update({i:
True for i, _
in enumerate(self.
_patterns[
"content"])})
430 text =
"\n".join(
"%r" % (v, )
for _, v, _
in api.iter_message_fields(msg, flat=
True))
434 WRAPS = []
if not self.
_highlight else self.
args.MATCH_WRAPPER
if not self.
sink else \
435 (common.MatchMarkers.START, common.MatchMarkers.END)
436 WRAPS = WRAPS
if isinstance(WRAPS, (list, tuple))
else []
if WRAPS
is None else [WRAPS]
437 WRAPS = ((WRAPS
or [
""]) * 2)[:2]
439 result, matched = copy.deepcopy(msg), {}
440 process_message(result)
441 yes =
not matched
if self.
args.INVERT
else len(matched) == len(self.
_patterns[
"content"])
442 return (result
if self.
_highlight else msg)
if yes
else None
445 __all__ = [
"Scanner"]