5------------------------------------------------------------------------------
6This file is part of grepros - grep for ROS bag files and live topics.
7Released under the BSD License.
12------------------------------------------------------------------------------
14## @namespace grepros.main
23from . import __title__, __version__, __version_date__, api, inputs, outputs, search
24from . common import ArgumentUtil, ConsolePrinter, MatchMarkers
29## Configuration for argparse, as {description, epilog, args: [..], groups: {name: [..]}}
31 "description":
"Searches through messages in ROS bag files or live topics.",
33PATTERNs use Python regular expression syntax, message matches if all match.
34* wildcards use simple globbing as zero or more characters,
35target matches if any value matches.
40Search for "my text" in all bags under current directory and subdirectories:
41 %(title)s -r "my text"
43Print 30 lines of the first message
from each live ROS topic:
44 %(title)s --max-per-topic 1 --lines-per-message 30 --live
46Find first message containing
"future" (case-sensitive)
in my.bag:
47 %(title)s future -I --max-count 1 --name my.bag
49Find 10 messages,
from geometry_msgs package,
in "map" frame,
50from bags
in current directory, reindexing any unindexed bags:
51 %(title)s frame_id=map --type geometry_msgs/* --max-count 10 --reindex-
if-unindexed
53Pipe all diagnostics messages
with "CPU usage" from live ROS topics to my.bag:
54 %(title)s
"CPU usage" --type *DiagnosticArray --no-console-output --write my.bag
56Find messages
with field
"key" containing
"0xA002",
57in topics ending
with "diagnostics",
in bags under
"/tmp":
58 %(title)s key=0xA002 --topic *diagnostics --path /tmp
60Find diagnostics_msgs messages
in bags
in current directory,
61containing
"navigation" in fields
"name" or "message",
62print only header stamp
and values:
63 %(title)s --type diagnostic_msgs/* --select-field name message \\
64 --emit-field header.stamp status.values -- navigation
66Print first message
from each lidar topic on ROS1 host 1.2.3.4, without highlight:
67 ROS_MASTER_URI=http://1.2.3.4::11311 \\
68 %(title)s --live --topic *lidar* --max-per-topic 1 --no-highlight
70Export all bag messages to SQLite
and Postgres,
print only export progress:
71 %(title)s -n my.bag --write my.bag.sqlite --no-console-output --no-verbose --progress
73 %(title)s -n my.bag --write postgresql://user
@host/dbname \\
74 --no-console-output --no-verbose --progress
75 """ % dict(title=__title__),
78 dict(args=[
"PATTERN"], nargs=
"*", default=[],
79 help=
"pattern(s) to find in message field values,\n"
80 "all messages match if not given,\n"
81 "can specify message field as NAME=PATTERN\n"
82 "(supports nested.paths and * wildcards)"),
84 dict(args=[
"-h",
"--help"],
85 dest=
"HELP", action=
"store_true",
86 help=
"show this help message and exit"),
88 dict(args=[
"-F",
"--fixed-strings"],
89 dest=
"FIXED_STRING", action=
"store_true",
90 help=
"PATTERNs are ordinary strings, not regular expressions"),
92 dict(args=[
"-I",
"--no-ignore-case"],
93 dest=
"CASE", action=
"store_true",
94 help=
"use case-sensitive matching in PATTERNs"),
96 dict(args=[
"-v",
"--invert-match"],
97 dest=
"INVERT", action=
"store_true",
98 help=
"select messages not matching PATTERNs"),
100 dict(args=[
"-e",
"--expression"],
101 dest=
"EXPRESSION", action=
"store_true",
102 help=
"PATTERNs are a logical expression\n"
103 "like 'this AND (this2 OR NOT \"skip this\")',\n"
104 "with elements as patterns to find in message fields"),
106 dict(args=[
"--version"],
107 dest=
"VERSION", action=
"version",
108 version=
"%s: grep for ROS bag files and live topics, v%s (%s)" %
109 (__title__, __version__, __version_date__),
110 help=
"display version information and exit"),
112 dict(args=[
"--live"],
113 dest=
"LIVE", action=
"store_true",
114 help=
"read messages from live ROS topics instead of bagfiles"),
116 dict(args=[
"--publish"],
117 dest=
"PUBLISH", action=
"store_true",
118 help=
"publish matched messages to live ROS topics"),
120 dict(args=[
"--write"],
121 dest=
"WRITE", nargs=
"+", default=[], action=
"append",
122 metavar=
"TARGET [format=bag] [KEY=VALUE ...]",
123 help=
"write matched messages to specified output,\n"
124 "format is autodetected from TARGET if not specified.\n"
125 "Bag or database will be appended to if it already exists.\n"
126 "Keyword arguments are given to output writer."),
128 dict(args=[
"--write-options"],
129 dest=
"WRITE_OPTIONS", default=argparse.SUPPRESS, help=argparse.SUPPRESS),
131 dict(args=[
"--plugin"],
132 dest=
"PLUGIN", nargs=
"+", default=[], action=
"append",
133 help=
"load a Python module or class as plugin"),
135 dict(args=[
"--stop-on-error"],
136 dest=
"STOP_ON_ERROR", action=
"store_true",
137 help=
"stop further execution on any error like unknown message type"),
140 "groups": {
"Filtering": [
142 dict(args=[
"-t",
"--topic"],
143 dest=
"TOPIC", nargs=
"+", default=[], action=
"append",
144 help=
"ROS topics to read if not all (supports * wildcards)"),
146 dict(args=[
"-nt",
"--no-topic"],
147 dest=
"SKIP_TOPIC", metavar=
"TOPIC", nargs=
"+", default=[], action=
"append",
148 help=
"ROS topics to skip (supports * wildcards)"),
150 dict(args=[
"-d",
"--type"],
151 dest=
"TYPE", nargs=
"+", default=[], action=
"append",
152 help=
"ROS message types to read if not all (supports * wildcards)"),
154 dict(args=[
"-nd",
"--no-type"],
155 dest=
"SKIP_TYPE", metavar=
"TYPE", nargs=
"+", default=[], action=
"append",
156 help=
"ROS message types to skip (supports * wildcards)"),
158 dict(args=[
"--condition"],
159 dest=
"CONDITION", nargs=
"+", default=[], action=
"append",
160 help=
"extra conditions to require for matching messages,\n"
161 "as ordinary Python expressions, can refer to last messages\n"
162 "in topics as <topic /my/topic>; topic name can contain wildcards.\n"
163 'E.g. --condition "<topic /robot/enabled>.data" matches\n'
164 "messages only while last message in '/robot/enabled' has data=true."),
166 dict(args=[
"-t0",
"--start-time"],
167 dest=
"START_TIME", metavar=
"TIME",
168 help=
"earliest timestamp of messages to read\n"
169 "as relative seconds if signed,\n"
170 "or epoch timestamp or ISO datetime\n"
171 "(for bag input, relative to bag start time\n"
172 "if positive or end time if negative,\n"
173 "for live input relative to system time,\n"
174 "datetime may be partial like 2021-10-14T12)"),
176 dict(args=[
"-t1",
"--end-time"],
177 dest=
"END_TIME", metavar=
"TIME",
178 help=
"latest timestamp of messages to read\n"
179 "as relative seconds if signed,\n"
180 "or epoch timestamp or ISO datetime\n"
181 "(for bag input, relative to bag start time\n"
182 "if positive or end time if negative,\n"
183 "for live input relative to system time,\n"
184 "datetime may be partial like 2021-10-14T12)"),
186 dict(args=[
"-n0",
"--start-index"],
187 dest=
"START_INDEX", metavar=
"INDEX", type=int,
188 help=
"message index within topic to start from\n"
189 "(1-based if positive, counts back from bag total if negative)"),
191 dict(args=[
"-n1",
"--end-index"],
192 dest=
"END_INDEX", metavar=
"INDEX", type=int,
193 help=
"message index within topic to stop at\n"
194 "(1-based if positive, counts back from bag total if negative)"),
196 dict(args=[
"--every-nth-message"],
197 dest=
"NTH_MESSAGE", metavar=
"NUM", type=int, default=1,
198 help=
"read every Nth message within topic, starting from first"),
200 dict(args=[
"--every-nth-interval"],
201 dest=
"NTH_INTERVAL", metavar=
"SECONDS", type=float, default=0,
202 help=
"read messages at least N seconds apart within topic"),
204 dict(args=[
"--every-nth-match"],
205 dest=
"NTH_MATCH", metavar=
"NUM", type=int, default=1,
206 help=
"emit every Nth match in topic, starting from first"),
208 dict(args=[
"-sf",
"--select-field"],
209 dest=
"SELECT_FIELD", metavar=
"FIELD", nargs=
"+", default=[], action=
"append",
210 help=
"message fields to use in matching if not all\n"
211 "(supports nested.paths and * wildcards)"),
213 dict(args=[
"-ns",
"--no-select-field"],
214 dest=
"NOSELECT_FIELD", metavar=
"FIELD", nargs=
"+", default=[], action=
"append",
215 help=
"message fields to skip in matching\n"
216 "(supports nested.paths and * wildcards)"),
218 dict(args=[
"-m",
"--max-count"],
219 dest=
"MAX_COUNT", metavar=
"NUM", default=0, type=int,
220 help=
"number of matched messages to emit (per each file if bag input)"),
222 dict(args=[
"--max-per-topic"],
223 dest=
"MAX_PER_TOPIC", metavar=
"NUM", default=0, type=int,
224 help=
"number of matched messages to emit from each topic\n"
225 "(per each file if bag input)"),
227 dict(args=[
"--max-topics"],
228 dest=
"MAX_TOPICS", metavar=
"NUM", default=0, type=int,
229 help=
"number of topics to emit matches from (per each file if bag input)"),
231 dict(args=[
"--unique-only"],
232 dest=
"UNIQUE", action=
"store_true",
233 help=
"only emit matches that are unique in topic,\n"
234 "taking --select-field and --no-select-field into account\n"
235 "(per each file if bag input)"),
237 ],
"Output control": [
239 dict(args=[
"-B",
"--before-context"],
240 dest=
"BEFORE", metavar=
"NUM", default=0, type=int,
241 help=
"emit NUM messages of leading context before match"),
243 dict(args=[
"-A",
"--after-context"],
244 dest=
"AFTER", metavar=
"NUM", default=0, type=int,
245 help=
"emit NUM messages of trailing context after match"),
247 dict(args=[
"-C",
"--context"],
248 dest=
"CONTEXT", metavar=
"NUM", default=0, type=int,
249 help=
"emit NUM messages of leading and trailing context\n"
252 dict(args=[
"-ef",
"--emit-field"],
253 dest=
"EMIT_FIELD", metavar=
"FIELD", nargs=
"+", default=[], action=
"append",
254 help=
"message fields to emit in console output if not all\n"
255 "(supports nested.paths and * wildcards)"),
257 dict(args=[
"-nf",
"--no-emit-field"],
258 dest=
"NOEMIT_FIELD", metavar=
"FIELD", nargs=
"+", default=[], action=
"append",
259 help=
"message fields to skip in console output\n"
260 "(supports nested.paths and * wildcards)"),
262 dict(args=[
"-mo",
"--matched-fields-only"],
263 dest=
"MATCHED_FIELDS_ONLY", action=
"store_true",
264 help=
"emit only the fields where PATTERNs find a match in console output"),
266 dict(args=[
"-la",
"--lines-around-match"],
267 dest=
"LINES_AROUND_MATCH", metavar=
"NUM", type=int,
268 help=
"emit only matched fields and NUM message lines\n"
269 "around match in console output"),
271 dict(args=[
"-lf",
"--lines-per-field"],
272 dest=
"MAX_FIELD_LINES", metavar=
"NUM", type=int,
273 help=
"maximum number of lines to emit per field in console output"),
275 dict(args=[
"-l0",
"--start-line"],
276 dest=
"START_LINE", metavar=
"NUM", type=int,
277 help=
"message line number to start emitting from in console output\n"
278 "(1-based if positive, counts back from total if negative)"),
280 dict(args=[
"-l1",
"--end-line"],
281 dest=
"END_LINE", metavar=
"NUM", type=int,
282 help=
"message line number to stop emitting at in console output\n"
283 "(1-based if positive, counts back from total if negative)"),
285 dict(args=[
"-lm",
"--lines-per-message"],
286 dest=
"MAX_MESSAGE_LINES", metavar=
"NUM", type=int,
287 help=
"maximum number of lines to emit per message in console output"),
289 dict(args=[
"--match-wrapper"],
290 dest=
"MATCH_WRAPPER", metavar=
"STR", nargs=
"*",
291 help=
"string to wrap around matched values in console output,\n"
292 "both sides if one value, start and end if more than one,\n"
293 "or no wrapping if zero values\n"
294 '(default "**" in colorless output)'),
296 dict(args=[
"--wrap-width"],
297 dest=
"WRAP_WIDTH", metavar=
"NUM", type=int,
298 help=
"character width to wrap message YAML console output at,\n"
299 "0 disables (defaults to detected terminal width)"),
301 dict(args=[
"--color"], dest=
"COLOR",
302 choices=[
"auto",
"always",
"never"], default=
"always",
303 help=
'use color output in console (default "always")'),
305 dict(args=[
"--no-meta"], dest=
"META", action=
"store_false",
306 help=
"do not print source and message metainfo to console"),
308 dict(args=[
"--no-filename"], dest=
"LINE_PREFIX", action=
"store_false",
309 help=
"do not print bag filename prefix on each console message line"),
311 dict(args=[
"--no-highlight"], dest=
"HIGHLIGHT", action=
"store_false",
312 help=
"do not highlight matched values"),
314 dict(args=[
"--no-console-output"], dest=
"CONSOLE", action=
"store_false",
315 help=
"do not print matches to console"),
317 dict(args=[
"--progress"], dest=
"PROGRESS", action=
"store_true",
318 help=
"show progress bar when not printing matches to console"),
320 dict(args=[
"--verbose"], dest=
"VERBOSE", action=
"store_true",
321 help=
"print status messages during console output\n"
322 "for publishing and writing, and error stacktraces"),
324 dict(args=[
"--no-verbose"], dest=
"SKIP_VERBOSE", action=
"store_true",
325 help=
"do not print status messages during console output\n"
326 "for publishing and writing"),
328 ],
"Bag input control": [
330 dict(args=[
"-n",
"--filename"],
331 dest=
"FILE", nargs=
"+", default=[], action=
"append",
332 help=
"names of ROS bagfiles to read if not all in directory\n"
333 "(supports * wildcards)"),
335 dict(args=[
"-p",
"--path"],
336 dest=
"PATH", nargs=
"+", default=[], action=
"append",
337 help=
"paths to scan if not current directory\n"
338 "(supports * wildcards)"),
340 dict(args=[
"-r",
"--recursive"],
341 dest=
"RECURSE", action=
"store_true",
342 help=
"recurse into subdirectories when looking for bagfiles"),
344 dict(args=[
"--order-bag-by"],
345 dest=
"ORDERBY", choices=[
"topic",
"type"],
346 help=
"order bag messages by topic or type first and then by time"),
348 dict(args=[
"--decompress"],
349 dest=
"DECOMPRESS", action=
"store_true",
350 help=
"decompress archived bagfiles with recognized extensions (.zst .zstd)"),
352 dict(args=[
"--reindex-if-unindexed"],
353 dest=
"REINDEX", action=
"store_true",
354 help=
"reindex unindexed bagfiles (ROS1 only), makes backup copies"),
356 dict(args=[
"--time-scale"],
357 dest=
"TIMESCALE", metavar=
"FACTOR", nargs=
"?", type=float, const=1, default=0,
358 help=
"emit messages on original bag timeline from first matched message,\n"
359 "optionally with a speedup or slowdown factor"),
361 dict(args=[
"--time-scale-emission"],
362 dest=
"TIMESCALE_EMISSION", nargs=
"?", type=int, const=
True, default=
True,
363 help=argparse.SUPPRESS),
365 ],
"Live topic control": [
367 dict(args=[
"--publish-prefix"],
368 dest=
"PUBLISH_PREFIX", metavar=
"PREFIX", default=
"",
369 help=
"prefix to prepend to input topic name on publishing match"),
371 dict(args=[
"--publish-suffix"],
372 dest=
"PUBLISH_SUFFIX", metavar=
"SUFFIX", default=
"",
373 help=
"suffix to append to input topic name on publishing match"),
375 dict(args=[
"--publish-fixname"],
376 dest=
"PUBLISH_FIXNAME", metavar=
"TOPIC", default=
"",
377 help=
"single output topic name to publish all matches to,\n"
378 "overrides prefix and suffix"),
380 dict(args=[
"--queue-size-in"],
381 dest=
"QUEUE_SIZE_IN", metavar=
"SIZE", type=int, default=10,
382 help=
"live ROS topic subscriber queue size (default 10)"),
384 dict(args=[
"--queue-size-out"],
385 dest=
"QUEUE_SIZE_OUT", metavar=
"SIZE", type=int, default=10,
386 help=
"output publisher queue size (default 10)"),
388 dict(args=[
"--ros-time-in"],
389 dest=
"ROS_TIME_IN", action=
"store_true",
390 help=
"use ROS time instead of system time for incoming message\n"
391 "timestamps from subsribed live ROS topics"),
401 """Writes a linefeed to sdtout if nothing has been printed to it so far."""
402 if not ConsolePrinter.PRINTS.get(sys.stdout)
and not sys.stdout.isatty():
404 except (Exception, KeyboardInterrupt):
pass
408 """Imports and initializes plugins from auto-load folder and from arguments."""
410 (
"overwrite=true|false",
"overwrite existing file\nin bag output\n"
411 "instead of appending to if bag or database\n"
412 "or appending unique counter to file name\n"
415 ] + outputs.RolloverSinkMixin.get_write_options(
"bag"))
417 if "--plugin" in cli_args:
418 args, _ = ArgumentUtil.make_parser(ARGUMENTS).parse_known_args(cli_args)
419 args = ArgumentUtil.flatten(args)
420 try: plugins.init(args)
421 except ImportWarning: sys.exit(1)
425 """Returns thread exception handler: function(text, exc) prints error, stops application."""
426 def thread_excepthook(text, exc):
427 """Prints error, sets exitcode flag, shuts down ROS node if any, interrupts main thread."""
428 ConsolePrinter.error(text)
429 if args.VERBOSE: traceback.print_exc()
430 exitcode_dict[
"value"] = 1
432 os.kill(os.getpid(), signal.SIGINT)
433 return thread_excepthook
437 """Parses command-line arguments and runs search."""
439 CLI_ARGS = sys.argv[1:]
440 MatchMarkers.populate(
"%08x" % random.randint(1, 1E9))
442 argparser = ArgumentUtil.make_parser(ARGUMENTS)
444 argparser.print_usage()
447 atexit.register(flush_stdout)
448 args = argparser.parse_args(CLI_ARGS)
450 argparser.print_help()
453 BREAK_EXS = (KeyboardInterrupt, )
454 try: BREAK_EXS += (BrokenPipeError, )
455 except NameError:
pass
457 exitcode = {
"value": 0}
458 source, sink =
None,
None
460 ConsolePrinter.configure({
"always":
True,
"never":
False}.get(args.COLOR))
462 args = ArgumentUtil.validate(args, cli=
True)
464 source = plugins.load(
"source", args)
or \
466 if not source.validate():
469 sink.sinks.extend(filter(bool, plugins.load(
"sink", args, collect=
True)))
470 if not sink.validate():
475 grepper.work(source, sink)
477 try: sink
and sink.close()
478 except (Exception, KeyboardInterrupt):
pass
479 try: source
and source.close()
480 except (Exception, KeyboardInterrupt):
pass
482 try: os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno())
483 except (Exception, KeyboardInterrupt):
pass
484 sys.exit(exitcode[
"value"])
485 except Exception
as e:
486 ConsolePrinter.error(e)
487 if args.VERBOSE: traceback.print_exc()
489 sink
and sink.close()
490 source
and source.close()
491 try: api.shutdown_node()
492 except BREAK_EXS:
pass
496 "ARGUMENTS",
"CLI_ARGS",
"flush_stdout",
"make_thread_excepthook",
"preload_plugins",
"run",
501if "__main__" == __name__:
Writes messages to bagfile.
Combines any number of sinks.
make_thread_excepthook(args, exitcode_dict)
Returns thread exception handler: function(text, exc) prints error, stops application.
preload_plugins(cli_args)
Imports and initializes plugins from auto-load folder and from arguments.
flush_stdout()
Writes a linefeed to sdtout if nothing has been printed to it so far.
run()
Parses command-line arguments and runs search.