QFPEKXL5OUKLT4WECMATSOHWYM24QPHKS6WZAAI5BAEQSAGAK6CQC DD3LCTLZFDIPVXXSG7RDINZCQ7NGKVG3X52OFVGVZIISD5VPF35QC ZOSYO3IBH5SCB27UP642O2SGCWQ7ZTQJSZ3PXKZVFQP72S34U3IQC KZKLAINJJWZ64T5MUZT34LJVQIKBTKZ6EJGD7C7TTSSDGCHEDPMAC JAT3DXOLENZZGXE2NYFF3TVQAQIXMMNYO234ETKQGC2CRHJVZERQC GE3VNRXLBCRRCW3R5CFGYKCAZE2WCQNTKTUW4BKV5GEN3NZKWXTAC 2P27XV3DGJCRA4SNJENCJYZLPR2XWZMTY7CGYYSJOY4UMDVVO25AC GPQSOVBPY7VTPHD75R6VWSNITPOL3AECF4DHJB32MF5Z72NV7YMQC SMWSHUOWFAP4GURQK3IBTSCIASVETCKBJWZLZKHH4WVOCRWW3BRAC YE6BZJUKQ7VMYEKKI3WSKTZEBR5NWUUDIN6PGE4W7OTPIY5N3NJQC NS4TDPLNAWJYJN37PZDYXMG6OJSAWZCMTPSPKX73JCLZZAMY25BAC requireFlags(fs, "--db", *dbPath, "--dataset", *datasetID, "--location", *locationID, "--cluster", *clusterID, "--file", *filePath)
if err := checkFlags(fs, "--db", *dbPath, "--dataset", *datasetID, "--location", *locationID, "--cluster", *clusterID, "--file", *filePath); err != nil {return err}
requireFlags(fs, "--db", *dbPath, "--dataset", *datasetID, "--location", *locationID, "--cluster", *clusterID, "--folder", *folderPath)
if err := checkFlags(fs, "--db", *dbPath, "--dataset", *datasetID, "--location", *locationID, "--cluster", *clusterID, "--folder", *folderPath); err != nil {return err}
requireFlags(fs, "--db", *dbPath, "--dataset", *datasetID, "--location", *locationID, "--cluster", *clusterID, "--folder", *folderPath, "--mapping", *mappingPath)
if err := checkFlags(fs, "--db", *dbPath, "--dataset", *datasetID, "--location", *locationID, "--cluster", *clusterID, "--folder", *folderPath, "--mapping", *mappingPath); err != nil {return err}
// requireFlags checks that the given flag values are non-empty strings.// If any are empty, prints usage and exits.// Each pair is (flagName, flagValue) — e.g. requireFlags(fs, "--db", *dbPath, "--id", *id)func requireFlags(fs *flag.FlagSet, pairs ...string) {var missing []stringfor i := 0; i < len(pairs); i += 2 {if pairs[i+1] == "" {missing = append(missing, pairs[i])}
// mustValue returns the value for a flag that requires an argument, advancing i by 2.// Exits the program if the value is missing (unrecoverable CLI error).func mustValue(args []string, i *int, flag string) string {if *i+1 >= len(args) {fmt.Fprintf(os.Stderr, "Error: %s requires a value\n", flag)os.Exit(1)
if len(missing) > 0 {fmt.Fprintf(os.Stderr, "Error: missing required flags: %v\n\n", missing)fs.Usage()
v := args[*i+1]*i += 2return v}// mustIntValue returns the integer value for a flag, validating range [lo,hi].// Exits on missing value, parse error, or out-of-range.func mustIntValue(args []string, i *int, flag string, lo, hi int) int {val := mustValue(args, i, flag)v, err := strconv.Atoi(val)if err != nil {fmt.Fprintf(os.Stderr, "Error: %s must be an integer\n", flag)
}// requireValue returns the next argument after a flag, or exits with an errorfunc requireValue(flag string, args []string, i *int) string {if *i+1 >= len(args) {fmt.Fprintf(os.Stderr, "Error: %s requires a value\n", flag)os.Exit(1)}v := args[*i+1]*i += 2return v
missing := []string{}if *from == "" {missing = append(missing, "--from")}if *to == "" {missing = append(missing, "--to")}if *species == "" {missing = append(missing, "--species")}if len(missing) > 0 {fmt.Fprintf(os.Stderr, "Error: missing required flags: %v\n\n", missing)fs.Usage()os.Exit(1)
if err := checkFlags(fs, "--from", *from, "--to", *to, "--species", *species); err != nil {return err
}// requireFlagValue extracts the value for a flag that requires an argument.// Exits on missing value.func requireFlagValue(args []string, i int, flagName string) string {if i+1 >= len(args) {fmt.Fprintf(os.Stderr, "Error: %s requires a value\n", flagName)os.Exit(1)}return args[i+1]
if *folder == "" {fmt.Fprintf(os.Stderr, "Error: --folder is required\n\n")fs.Usage()os.Exit(1)}if *mapping == "" {fmt.Fprintf(os.Stderr, "Error: --mapping is required\n\n")fs.Usage()os.Exit(1)
if err := checkFlags(fs, "--folder", *folder, "--mapping", *mapping); err != nil {return err
// validateClipFlags checks required flags and flag combinationsfunc validateClipFlags(f clipFlags) {
// validateClipFlags checks required flags and flag combinations.// Returns an error instead of exiting.func validateClipFlags(f clipFlags) error {
}// mustUniqueValue is like mustValue but exits if the flag was already set.func mustUniqueValue(args []string, i *int, flag, current string) string {if current != "" {fmt.Fprintf(os.Stderr, "Error: %s can only be specified once\n", flag)os.Exit(1)}return mustValue(args, i, flag)
// requireValue returns the next argument or exits if missing.func (classifyArgs) requireValue(args []string, i int, flag string) string {if i+1 >= len(args) {fmt.Fprintf(os.Stderr, "Error: %s requires a value\n", flag)os.Exit(1)}return args[i+1]}// requireUniqueValue is like requireValue but exits if the flag was already set.func (a classifyArgs) requireUniqueValue(args []string, i int, flag, current string) string {if current != "" {fmt.Fprintf(os.Stderr, "Error: %s can only be specified once\n", flag)os.Exit(1)}return a.requireValue(args, i, flag)}// requireIntRange parses the next arg as int and validates range [lo,hi].func (a classifyArgs) requireIntRange(args []string, i int, flag string, lo, hi int) int {val := a.requireValue(args, i, flag)v, err := strconv.Atoi(val)if err != nil {fmt.Fprintf(os.Stderr, "Error: %s must be an integer\n", flag)os.Exit(1)}if v < lo || v > hi {fmt.Fprintf(os.Stderr, "Error: %s must be between %d and %d\n", flag, lo, hi)os.Exit(1)}return v}
// the converted []tools.KeyBinding slice. Exits on validation errors.func validateBindings(cfg *utils.Config, cfgPath string) []tools.KeyBinding {
// the converted []tools.KeyBinding slice. Returns an error on validation errors.func validateBindings(cfg *utils.Config, cfgPath string) ([]tools.KeyBinding, error) {
fmt.Fprintf(os.Stderr, "Error: Could not parse filter from filename. Use --filter flag.\n")fmt.Fprintf(os.Stderr, "Expected format: prefix_filter_date.csv (e.g., predsST_opensoundscape-kiwi-1.2_2025-11-12.csv)\n")os.Exit(1)
return fmt.Errorf("could not parse filter from filename. Use --filter flag. Expected format: prefix_filter_date.csv (e.g., predsST_opensoundscape-kiwi-1.2_2025-11-12.csv)")
## [2026-05-11] Stream 3c complete: os.Exit elimination in cmd/Completed the conversion of all cmd/ subcommand functions from void+os.Exitto error-return pattern. This extends the Stream 3c work that previouslyconverted core CRUD + utility commands.**os.Exit count in cmd/:** 172 → 13 (remaining are in CLI arg-parsing helpers:`mustValue`, `mustIntValue`, `mustUniqueValue` — these handle truly fatalsyntax errors like missing flag values, duplicate flags, unknown flags)**All business logic errors** now flow through main.go's single exit point.**New shared helpers in `cmd/common.go`:**- `mustValue(args, &i, flag)` — returns next arg, advances i, exits on missing- `mustIntValue(args, &i, flag, lo, hi)` — returns validated int, exits on error- `mustUniqueValue(args, &i, flag, current)` — exits if flag already set- `checkFlags(fs, pairs...)` — returns error if any string flags are empty- `checkNonZeroFlags(fs, pairs...)` — returns error if any int flags are zero- Removed `requireFlags` (replaced by `checkFlags` everywhere)**Converted functions** (void→error-return):- `RunCalls` dispatcher + all subcommand handlers- `RunCallsClassify`, `RunCallsClip`, `RunCallsModify`- `runCallsFromPreds`, `runCallsFromBirda`, `runCallsFromRaven`- `runCallsShowImages`, `runCallsSummarise`- `runCallsClipLabels`, `runCallsPropagate`- `runCallsDetectAnomalies`, `runCallsPushCertainty`- `RunImport` dispatcher + all subcommand handlers- `runImportBulk`, `runImportFile`, `runImportFolder`- `runImportSegments`, `runImportUnstructured`- `printJSON` now returns error**main.go:** Added `ErrHelpRequested` sentinel — `--help` exits with code 0via `errors.Is(err, cmd.ErrHelpRequested)`.
**Complexity reductions:**- `parseClassifyArgs`: 22 → 13 (extracted `mustIntValue`, `mustUniqueValue`)- `parseModifyArgs`: 21 → 9 (extracted `mustValue`, `mustIntValue`)- `parsePushCertaintyArgs`: 17 → 9 (extracted `mustValue`)