ref(sendsms): hard error on leftover template strings, other output improvements

This commit is contained in:
AJ ONeal 2026-01-24 22:24:00 -07:00
parent 0c3c436c60
commit a050e5d0c7
No known key found for this signature in database

View File

@ -74,6 +74,7 @@ const (
fgRed = "\033[31m" fgRed = "\033[31m"
textErr = textBold + fgRed textErr = textBold + fgRed
textWarn = textBold + fgYellow textWarn = textBold + fgYellow
textInfo = fgYellow
textPrompt = fgBlue textPrompt = fgBlue
) )
@ -115,17 +116,17 @@ func main() {
{ {
cfg.startTime, err = parseClock(cfg.startClock, now) cfg.startTime, err = parseClock(cfg.startClock, now)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "%sError%s: could not use --start-time %q: %v\n", textErr, textReset, cfg.startClock, err) fmt.Fprintf(os.Stderr, "\n%sError%s: could not use --start-time %q: %v\n", textErr, textReset, cfg.startClock, err)
os.Exit(1) os.Exit(1)
} }
cfg.endTime, err = parseClock(cfg.endClock, now) cfg.endTime, err = parseClock(cfg.endClock, now)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "%sError%s: could not use --end-time %q: %v\n", textErr, textReset, cfg.endClock, err) fmt.Fprintf(os.Stderr, "\n%sError%s: could not use --end-time %q: %v\n", textErr, textReset, cfg.endClock, err)
os.Exit(1) os.Exit(1)
} }
if cfg.startTime.After(cfg.endTime) || cfg.startTime.Equal(cfg.endTime) { if cfg.startTime.After(cfg.endTime) || cfg.startTime.Equal(cfg.endTime) {
fmt.Fprintf(os.Stderr, fmt.Fprintf(os.Stderr,
"%sError%s: no time between --start-time %q and --end-time %q\n", textErr, textReset, cfg.startTime, cfg.endTime) "\n%sError%s: no time between --start-time %q and --end-time %q\n", textErr, textReset, cfg.startTime, cfg.endTime)
os.Exit(1) os.Exit(1)
} }
} }
@ -140,7 +141,7 @@ func main() {
fmt.Fprintf(os.Stderr, "Info: opening, reading, and parsing %q\n", cfg.csvPath) fmt.Fprintf(os.Stderr, "Info: opening, reading, and parsing %q\n", cfg.csvPath)
file, err := os.Open(cfg.csvPath) file, err := os.Open(cfg.csvPath)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "%sError%s: %v\n", textErr, textReset, err) fmt.Fprintf(os.Stderr, "\n%sError%s: %v\n", textErr, textReset, err)
os.Exit(1) os.Exit(1)
} }
defer func() { defer func() {
@ -151,7 +152,7 @@ func main() {
messages, warns, err := cfg.LaxParseCSV(csvr) messages, warns, err := cfg.LaxParseCSV(csvr)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err) fmt.Fprintf(os.Stderr, "\n%sError%s: %v\n", textErr, textReset, err)
os.Exit(1) os.Exit(1)
} }
if len(warns) > 0 { if len(warns) > 0 {
@ -161,14 +162,13 @@ func main() {
fmt.Fprintf(os.Stderr, " (pass --verbose to show warnings)\n") fmt.Fprintf(os.Stderr, " (pass --verbose to show warnings)\n")
} }
if cfg.verbose { if cfg.verbose {
fmt.Fprintf(os.Stderr, "\n")
for _, warn := range warns { for _, warn := range warns {
fmt.Fprintf(os.Stderr, "Skip: %s\n", warn.Message) fmt.Fprintf(os.Stderr, " Skip: %s\n", warn.Message)
} }
} }
fmt.Fprintf(os.Stderr, "\n") fmt.Fprintf(os.Stderr, "\n")
} }
fmt.Fprintf(os.Stderr, "Info: messages to send: %d\n", len(messages)) fmt.Fprintf(os.Stderr, "Info: list of %d messages\n", len(messages))
if now.After(cfg.endTime) || now.Equal(cfg.endTime) { if now.After(cfg.endTime) || now.Equal(cfg.endTime) {
fmt.Fprintf(os.Stderr, "%sWarning%s: Too late now. %sWaiting until tomorrow%s:\n", textWarn, textReset, textWarn, textReset) fmt.Fprintf(os.Stderr, "%sWarning%s: Too late now. %sWaiting until tomorrow%s:\n", textWarn, textReset, textWarn, textReset)
@ -179,7 +179,7 @@ func main() {
// check for issues caused by daylight savings // check for issues caused by daylight savings
if cfg.startTime.After(cfg.endTime) || cfg.startTime.Equal(cfg.endTime) { if cfg.startTime.After(cfg.endTime) || cfg.startTime.Equal(cfg.endTime) {
fmt.Fprintf(os.Stderr, fmt.Fprintf(os.Stderr,
"%sError%s: no time between --start-time %q and --end-time %q\n", "\n%sError%s: no time between --start-time %q and --end-time %q\n",
textErr, textReset, textErr, textReset,
cfg.startTime, cfg.endTime, cfg.startTime, cfg.endTime,
) )
@ -207,12 +207,12 @@ func main() {
} }
var startAgo = now.Sub(cfg.startTime) var startAgo = now.Sub(cfg.startTime)
if startAgo >= 0 { if startAgo >= 0 {
fmt.Fprintf(os.Stderr, "Info: start time was %s (%s ago)\n", cfg.startTime.Format("3:04pm"), startAgo.Round(time.Second)) fmt.Fprintf(os.Stderr, "Info: start after %s (%s ago)\n", cfg.startTime.Format("3:04pm"), startAgo.Round(time.Second))
} else { } else {
startAgo *= -1 startAgo *= -1
fmt.Fprintf(os.Stderr, "Info: start time is %s (%s from now)\n", cfg.startTime.Format("3:04pm"), startAgo.Round(time.Second)) fmt.Fprintf(os.Stderr, "Info: start after %s (%s from now)\n", cfg.startTime.Format("3:04pm"), startAgo.Round(time.Second))
} }
fmt.Fprintf(os.Stderr, "Info: end time is %s (%s from now)\n", cfg.endTime.Format("3:04pm"), cfg.duration.Round(time.Second)) fmt.Fprintf(os.Stderr, "Info: end around %s (%s from now)\n", cfg.endTime.Format("3:04pm"), cfg.duration.Round(time.Second))
} }
// delay // delay
@ -243,21 +243,25 @@ func main() {
}) })
} }
fmt.Fprintf(os.Stderr, "Info: average delay between messages: %s\n", cfg.delay.Round(time.Second))
quarterDelay := cfg.delay / 4 quarterDelay := cfg.delay / 4
baseDelay := quarterDelay * 3 baseDelay := quarterDelay * 3
jitter := int64(quarterDelay * 2) jitter := int64(quarterDelay * 2)
fmt.Fprintf(os.Stderr, fmt.Fprintf(os.Stderr,
" (%s minimum + %s jitter)\n", "Info: delay %s between messages (%s + %s jitter)\n",
baseDelay.Round(time.Millisecond), time.Duration(jitter).Round(time.Millisecond), cfg.delay.Round(time.Second), baseDelay.Round(time.Millisecond), time.Duration(jitter).Round(time.Millisecond),
) )
if len(messages) == 0 { if len(messages) == 0 {
fmt.Fprintf(os.Stderr, "\n") fmt.Fprintf(os.Stderr, "\n%sError%s: no messages to send\n", textErr, textReset)
fmt.Fprintf(os.Stderr, "%sError%s: no messages to send\n", textErr, textReset)
os.Exit(1) os.Exit(1)
} }
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, "Info: This is what a %ssample message%s from list look like:\n", textInfo, textReset)
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, " To: %s (%s)\n", messages[0].Number, messages[0].Name)
fmt.Fprintf(os.Stderr, " %s%s%s\n", textInfo, messages[0].Text, textReset)
if !cfg.confirmed && !cfg.dryRun { if !cfg.confirmed && !cfg.dryRun {
fmt.Fprintf(os.Stderr, "\n") fmt.Fprintf(os.Stderr, "\n")
if !confirmContinue() { if !confirmContinue() {
@ -283,7 +287,7 @@ func main() {
last := len(messages) last := len(messages)
left := last - cur left := last - cur
if left > 0 { if left > 0 {
fmt.Fprintf(os.Stderr, "%sError%s: Oh, look at the time. Ending now. (%d messages remaining)\n", textErr, textReset, left) fmt.Fprintf(os.Stderr, "\n%sError%s: Oh, look at the time. Ending now. (%d messages remaining)\n", textErr, textReset, left)
os.Exit(1) os.Exit(1)
return return
} }
@ -315,7 +319,7 @@ func main() {
} }
} }
fmt.Fprintf(os.Stderr, "finished at %s", time.Now()) fmt.Fprintf(os.Stderr, "Info: finished at %s\n", time.Now())
} }
func confirmContinue() bool { func confirmContinue() bool {
@ -395,7 +399,11 @@ type CSVWarn struct {
Record []string Record []string
} }
var reUnmatchedVars = regexp.MustCompile(`\{[^}]+\}`) func (w CSVWarn) Error() string {
return w.Message
}
var reUnmatchedVars = regexp.MustCompile(`(\{[^}]+\})`)
func (cfg *MainConfig) LaxParseCSV(csvr *csv.Reader) (messages []SMSMessage, warns []CSVWarn, err error) { func (cfg *MainConfig) LaxParseCSV(csvr *csv.Reader) (messages []SMSMessage, warns []CSVWarn, err error) {
header, err := csvr.Read() header, err := csvr.Read()
@ -407,7 +415,7 @@ func (cfg *MainConfig) LaxParseCSV(csvr *csv.Reader) (messages []SMSMessage, war
FIELD_PHONE := GetFieldIndex(header, "Phone") FIELD_PHONE := GetFieldIndex(header, "Phone")
FIELD_MESSAGE := GetFieldIndex(header, "Message") FIELD_MESSAGE := GetFieldIndex(header, "Message")
if FIELD_NAME == -1 || FIELD_PHONE == -1 || FIELD_MESSAGE == -1 { if FIELD_NAME == -1 || FIELD_PHONE == -1 || FIELD_MESSAGE == -1 {
return nil, nil, fmt.Errorf("header is missing one or more of 'Preferred', 'Phone', and/or 'Message'") return nil, nil, fmt.Errorf("header is missing one or more of 'Name', 'Phone', and/or 'Message'")
} }
FIELD_MIN := 1 + slices.Max([]int{FIELD_NAME, FIELD_PHONE, FIELD_MESSAGE}) FIELD_MIN := 1 + slices.Max([]int{FIELD_NAME, FIELD_PHONE, FIELD_MESSAGE})
@ -452,6 +460,7 @@ func (cfg *MainConfig) LaxParseCSV(csvr *csv.Reader) (messages []SMSMessage, war
Vars: vars, Vars: vars,
Text: strings.TrimSpace(rec[FIELD_MESSAGE]), Text: strings.TrimSpace(rec[FIELD_MESSAGE]),
} }
message.Text = replaceVar(message.Template, "Name", message.Name)
keyIter := maps.Keys(message.Vars) keyIter := maps.Keys(message.Vars)
keys := slices.Sorted(keyIter) keys := slices.Sorted(keyIter)
@ -460,14 +469,16 @@ func (cfg *MainConfig) LaxParseCSV(csvr *csv.Reader) (messages []SMSMessage, war
message.Text = replaceVar(message.Text, key, val) message.Text = replaceVar(message.Text, key, val)
} }
if reUnmatchedVars.MatchString(message.Text) { if tmpls := reUnmatchedVars.FindAllString(message.Text, -1); len(tmpls) != 0 {
warns = append(warns, CSVWarn{ return nil, nil, &CSVWarn{
Index: rowIndex, Index: rowIndex,
Code: "UnmatchedVars", Code: "UnmatchedVars",
Message: fmt.Sprintf("ignoring row %d: leftover template variables (e.g. {VarName})", rowIndex), Message: fmt.Sprintf(
"failing due to row %d (%s): leftover template variable(s): %s",
rowIndex, message.Name, strings.Join(tmpls, " "),
),
Record: rec, Record: rec,
}) }
continue
} }
message.Number = cleanPhoneNumber(message.Number) message.Number = cleanPhoneNumber(message.Number)