Skip to content

Commit ba3c9c0

Browse files
authored
CLI: accept RFC3339-formatted dates in relevant arguments (#1263)
### Changelog #### Added CLI: `mcap add attachment` accepts RFC3339-formatted dates to specify the log and creation times for the new attachment. CLI: `mcap filter` accepts RFC3339-formatted dates for start and end times. ### Docs None. ### Description Adds the ability for users to specify a date when filtering MCAP files, or adding attachments. I did a quick pass to see if there are any other arguments where we accept a date or date-like value, I can't see any more.
1 parent 693030f commit ba3c9c0

File tree

3 files changed

+102
-26
lines changed

3 files changed

+102
-26
lines changed

go/cli/mcap/cmd/attachment.go

+26-10
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ import (
1313
)
1414

1515
var (
16-
addAttachmentLogTime uint64
16+
addAttachmentLogTime string
1717
addAttachmentName string
18-
addAttachmentCreationTime uint64
18+
addAttachmentCreationTime string
1919
addAttachmentFilename string
2020
addAttachmentMediaType string
2121
)
@@ -155,12 +155,20 @@ var addAttachmentCmd = &cobra.Command{
155155
die("failed to stat file %s", addAttachmentFilename)
156156
}
157157
createTime := uint64(fi.ModTime().UTC().UnixNano())
158-
if addAttachmentCreationTime > 0 {
159-
createTime = addAttachmentCreationTime
158+
if addAttachmentCreationTime != "" {
159+
date, err := parseDateOrNanos(addAttachmentCreationTime)
160+
if err != nil {
161+
die("failed to parse creation date: %s", err)
162+
}
163+
createTime = date
160164
}
161165
logTime := uint64(time.Now().UTC().UnixNano())
162-
if addAttachmentLogTime > 0 {
163-
logTime = addAttachmentLogTime
166+
if addAttachmentLogTime != "" {
167+
date, err := parseDateOrNanos(addAttachmentCreationTime)
168+
if err != nil {
169+
die("failed to parse log date: %s", err)
170+
}
171+
logTime = date
164172
}
165173
err = utils.AmendMCAP(f, []*mcap.Attachment{
166174
{
@@ -187,11 +195,19 @@ func init() {
187195
addAttachmentCmd.PersistentFlags().StringVarP(
188196
&addAttachmentMediaType, "content-type", "", "application/octet-stream", "content type of attachment",
189197
)
190-
addAttachmentCmd.PersistentFlags().Uint64VarP(
191-
&addAttachmentLogTime, "log-time", "", 0, "attachment log time in nanoseconds (defaults to current timestamp)",
198+
addAttachmentCmd.PersistentFlags().StringVarP(
199+
&addAttachmentLogTime,
200+
"log-time",
201+
"",
202+
"",
203+
"attachment log time in nanoseconds or RFC3339 format (defaults to current timestamp)",
192204
)
193-
addAttachmentCmd.PersistentFlags().Uint64VarP(
194-
&addAttachmentLogTime, "creation-time", "", 0, "attachment creation time in nanoseconds (defaults to ctime)",
205+
addAttachmentCmd.PersistentFlags().StringVarP(
206+
&addAttachmentLogTime,
207+
"creation-time",
208+
"",
209+
"",
210+
"attachment creation time in nanoseconds or RFC3339 format (defaults to ctime)",
195211
)
196212
err := addAttachmentCmd.MarkPersistentFlagRequired("file")
197213
if err != nil {

go/cli/mcap/cmd/filter.go

+66-16
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"math"
99
"os"
1010
"regexp"
11+
"strconv"
12+
"time"
1113

1214
"github.com/foxglove/mcap/go/cli/mcap/utils"
1315
"github.com/foxglove/mcap/go/mcap"
@@ -22,6 +24,8 @@ type filterFlags struct {
2224
endSec uint64
2325
startNano uint64
2426
endNano uint64
27+
start string
28+
end string
2529
includeMetadata bool
2630
includeAttachments bool
2731
outputCompression string
@@ -41,20 +45,51 @@ type filterOpts struct {
4145
chunkSize int64
4246
}
4347

48+
// parseDateOrNanos parses a string containing either an RFC3339-formatted date with timezone
49+
// or a decimal number of nanoseconds. It returns a uint64 timestamp in nanoseconds.
50+
func parseDateOrNanos(dateOrNanos string) (uint64, error) {
51+
intNanos, err := strconv.ParseUint(dateOrNanos, 10, 64)
52+
if err == nil {
53+
return intNanos, nil
54+
}
55+
date, err := time.Parse(time.RFC3339, dateOrNanos)
56+
if err != nil {
57+
return 0, err
58+
}
59+
return uint64(date.UnixNano()), nil
60+
}
61+
62+
// parseTimestampArgs implements the semantics for setting start and end times in the CLI.
63+
// a non-default value in `dateOrNanos` overrides `nanoseconds`, which overrides `seconds`.
64+
func parseTimestampArgs(dateOrNanos string, nanoseconds uint64, seconds uint64) (uint64, error) {
65+
if dateOrNanos != "" {
66+
return parseDateOrNanos(dateOrNanos)
67+
}
68+
if nanoseconds != 0 {
69+
return nanoseconds, nil
70+
}
71+
return seconds * 1e9, nil
72+
}
73+
4474
func buildFilterOptions(flags *filterFlags) (*filterOpts, error) {
4575
opts := &filterOpts{
4676
output: flags.output,
4777
includeMetadata: flags.includeMetadata,
4878
includeAttachments: flags.includeAttachments,
4979
}
50-
opts.start = flags.startNano
5180
if flags.startSec > 0 {
5281
opts.start = flags.startSec * 1e9
5382
}
54-
opts.end = flags.endNano
55-
if flags.endSec > 0 {
56-
opts.end = flags.endSec * 1e9
83+
start, err := parseTimestampArgs(flags.start, flags.startNano, flags.startSec)
84+
if err != nil {
85+
return nil, fmt.Errorf("invalid start: %w", err)
5786
}
87+
opts.start = start
88+
end, err := parseTimestampArgs(flags.end, flags.endSec, flags.endNano)
89+
if err != nil {
90+
return nil, fmt.Errorf("invalid end: %w", err)
91+
}
92+
opts.end = end
5893
if opts.end == 0 {
5994
opts.end = math.MaxUint64
6095
}
@@ -392,30 +427,43 @@ usage:
392427
[]string{},
393428
"messages with topic names matching this regex will be excluded, can be supplied multiple times",
394429
)
430+
start := filterCmd.PersistentFlags().StringP(
431+
"start",
432+
"S",
433+
"",
434+
"only include messages logged at or after this time. Accepts integer nanoseconds or RFC3339-formatted date.",
435+
)
395436
startSec := filterCmd.PersistentFlags().Uint64P(
396437
"start-secs",
397438
"s",
398439
0,
399-
"messages with log times after or equal to this timestamp will be included.",
440+
"only include messages logged at or after this time. Accepts integer seconds."+
441+
"Ignored if `--start` or `--start-nsecs` are used.",
442+
)
443+
startNano := filterCmd.PersistentFlags().Uint64(
444+
"start-nsecs",
445+
0,
446+
"deprecated, use --start. Only include messages logged at or after this time. Accepts integer nanoseconds.",
447+
)
448+
end := filterCmd.PersistentFlags().StringP(
449+
"end",
450+
"E",
451+
"",
452+
"Only include messages logged before this time. Accepts integer nanoseconds or RFC3339-formatted date.",
400453
)
401454
endSec := filterCmd.PersistentFlags().Uint64P(
402455
"end-secs",
403456
"e",
404457
0,
405-
"messages with log times before timestamp will be included.",
458+
"only include messages logged before this time. Accepts integer seconds."+
459+
"Ignored if `--end` or `--end-nsecs` are used.",
406460
)
407-
startNano := filterCmd.PersistentFlags().Uint64P(
408-
"start-nsecs",
409-
"S",
410-
0,
411-
"messages with log times after or equal to this nanosecond-precision timestamp will be included.",
412-
)
413-
endNano := filterCmd.PersistentFlags().Uint64P(
461+
endNano := filterCmd.PersistentFlags().Uint64(
414462
"end-nsecs",
415-
"E",
416463
0,
417-
"messages with log times before nanosecond-precision timestamp will be included.",
464+
"(Deprecated, use --end) Only include messages logged before this time. Accepts integer nanosconds.",
418465
)
466+
419467
filterCmd.MarkFlagsMutuallyExclusive("start-secs", "start-nsecs")
420468
filterCmd.MarkFlagsMutuallyExclusive("end-secs", "end-nsecs")
421469
chunkSize := filterCmd.PersistentFlags().Int64P("chunk-size", "", 4*1024*1024, "chunk size of output file")
@@ -439,9 +487,11 @@ usage:
439487
output: *output,
440488
includeTopics: *includeTopics,
441489
excludeTopics: *excludeTopics,
490+
start: *start,
442491
startSec: *startSec,
443-
endSec: *endSec,
444492
startNano: *startNano,
493+
end: *end,
494+
endSec: *endSec,
445495
endNano: *endNano,
446496
chunkSize: *chunkSize,
447497
includeMetadata: *includeMetadata,

go/cli/mcap/cmd/filter_test.go

+10
Original file line numberDiff line numberDiff line change
@@ -368,3 +368,13 @@ func TestCompileMatchers(t *testing.T) {
368368
assert.True(t, matchers[0].MatchString("camera"))
369369
assert.True(t, matchers[1].MatchString("lights"))
370370
}
371+
372+
func TestParseDateOrNanos(t *testing.T) {
373+
expected := uint64(1690298850132545471)
374+
zulu, err := parseDateOrNanos("2023-07-25T15:27:30.132545471Z")
375+
require.NoError(t, err)
376+
assert.Equal(t, expected, zulu)
377+
withTimezone, err := parseDateOrNanos("2023-07-26T01:27:30.132545471+10:00")
378+
require.NoError(t, err)
379+
assert.Equal(t, expected, withTimezone)
380+
}

0 commit comments

Comments
 (0)