From 2f3e54d1f6b4ed6a0c49548627e83791a9024043 Mon Sep 17 00:00:00 2001 From: John Kerl Date: Wed, 15 Jul 2026 15:27:26 -0400 Subject: [PATCH] Remove os.Exit callsites below the entrypoint: phase 4 (plans/exit.md) (#2205) Phase 4 of plans/exit.md: the aux/terminal sub-entrypoints. pkg/auxents and pkg/terminals are now os.Exit-free; even the dispatchers return exit codes instead of exiting. - auxents.Dispatch returns (handled bool, exitCode int); entrypoint.Main -- the one sanctioned exit point -- exits with the code. - terminals.Dispatch returns an exit code; the climain caller wraps it in a lib.ExitRequest, which also retires the 'terminal did not exit the process' panic. - Usage functions (hex, unhex, lecat, termcvt, repl, script, regtest) no longer take an exitCode parameter and exit; call sites print usage and return the code explicitly. - Inner I/O helpers (hexDumpFile, unhexFile, lecatFile, termcvtFile) return errors; each sub-main prints 'mlr {verb}: ...' as before and returns 1. lecatFile formerly looped forever on a non-EOF read error; it now returns the error. - The regtest directory walk (Execute / executeSinglePath / executeSingleDirectory / hasCaseSubdirectories) plumbs errors up to RegTestMain; regression_test.go updated for the new Execute signature. Stderr messages and exit codes are unchanged across mlr aux-list, hex, unhex, lecat, termcvt, help, version, repl, script, and regtest surfaces (checked by hand); all 4779 regression cases pass; make lint is clean; docs rebuild with zero churn. Co-authored-by: Claude Fable 5 --- pkg/auxents/auxents.go | 15 +++++---- pkg/auxents/hex.go | 28 +++++++++------- pkg/auxents/lecat.go | 28 +++++++++++----- pkg/auxents/termcvt.go | 53 ++++++++++++++++-------------- pkg/auxents/unhex.go | 35 +++++++++++--------- pkg/climain/mlrcli_parse.go | 6 ++-- pkg/entrypoint/entrypoint.go | 6 ++-- pkg/terminals/regtest/entry.go | 24 +++++++++----- pkg/terminals/regtest/regtester.go | 53 +++++++++++++++++------------- pkg/terminals/repl/entry.go | 24 +++++++------- pkg/terminals/script/entry.go | 38 ++++++++++++--------- pkg/terminals/terminals.go | 13 +++++--- regression_test.go | 5 ++- 13 files changed, 195 insertions(+), 133 deletions(-) diff --git a/pkg/auxents/auxents.go b/pkg/auxents/auxents.go index 12c7545c5..97c53ff29 100644 --- a/pkg/auxents/auxents.go +++ b/pkg/auxents/auxents.go @@ -32,22 +32,23 @@ func init() { } } -// Dispatch is called from Miller main. Here we indicate if argv[1] is handled -// by us, or not. If so, we handle it and exit, not returning control to Miller -// main. -func Dispatch(args []string) { +// Dispatch is called from Miller main. The boolean return says whether +// argv[1] named an auxent handled here; if so, the int is the process exit +// code for the entrypoint layer to exit with. Otherwise control returns to +// main for the rest of Miller. +func Dispatch(args []string) (bool, int) { if len(args) < 2 { - return + return false, 0 } verb := args[1] for _, entry := range _AUX_LOOKUP_TABLE { if verb == entry.name { - os.Exit(entry.main(args)) + return true, entry.main(args) } } - // Else, return control to main for the rest of Miller. + return false, 0 } // auxListMain is the handler for 'mlr aux-list'. diff --git a/pkg/auxents/hex.go b/pkg/auxents/hex.go index caa9ac91e..9fad5a676 100644 --- a/pkg/auxents/hex.go +++ b/pkg/auxents/hex.go @@ -23,14 +23,13 @@ import ( // 00000060: 60 61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f |`abcdefghijklmno| // 00000070: 70 71 72 73 74 75 76 77 78 79 7a 7b 7c 7d 7e 7f |pqrstuvwxyz{|}~.| -func hexUsage(verbName string, o *os.File, exitCode int) { +func hexUsage(verbName string, o *os.File) { fmt.Fprintf(o, "Usage: mlr %s [options] {zero or more file names}\n", verbName) fmt.Fprintf(o, "Simple hex-dump.\n") fmt.Fprintf(o, "If zero file names are supplied, standard input is read.\n") fmt.Fprintf(o, "Options:\n") fmt.Fprintf(o, "-r: print only raw hex without leading offset indicators or trailing ASCII dump.\n") fmt.Fprintf(o, "-h or --help: print this message\n") - os.Exit(exitCode) } func hexMain(args []string) int { @@ -45,12 +44,16 @@ func hexMain(args []string) int { doRaw = true args = args[1:] case "-h", "--help": - hexUsage(verb, os.Stdout, 0) + hexUsage(verb, os.Stdout) + return 0 } } if len(args) == 0 { - hexDumpFile(os.Stdin, doRaw) + if err := hexDumpFile(os.Stdin, doRaw); err != nil { + fmt.Fprintf(os.Stderr, "mlr hex: %v\n", err) + return 1 + } } else { for _, filename := range args { // Print filename if there is more than one file, unless raw output @@ -61,14 +64,17 @@ func hexMain(args []string) int { istream, err := os.Open(filename) if err != nil { - // TODO: "mlr" fmt.Fprintf(os.Stderr, "mlr hex: %v\n", err) - os.Exit(1) + return 1 } - hexDumpFile(istream, doRaw) - + err = hexDumpFile(istream, doRaw) _ = istream.Close() + if err != nil { + fmt.Fprintf(os.Stderr, "mlr hex: %v\n", err) + return 1 + } + if !doRaw && len(args) > 1 { fmt.Println() } @@ -78,7 +84,7 @@ func hexMain(args []string) int { return 0 } -func hexDumpFile(istream *os.File, doRaw bool) { +func hexDumpFile(istream *os.File, doRaw bool) error { const bytesPerClump = 4 const clumpsPerLine = 4 const bufferSize = bytesPerClump * clumpsPerLine @@ -97,8 +103,7 @@ func hexDumpFile(istream *os.File, doRaw bool) { // exact multiple of our buffer size. We'll break the loop after // hex-dumping this last, partial fragment. if err != nil && err != io.ErrUnexpectedEOF { - fmt.Fprintf(os.Stderr, "mlr hex: %v\n", err) - os.Exit(1) + return err } // Print offset "pre" part @@ -148,4 +153,5 @@ func hexDumpFile(istream *os.File, doRaw bool) { offset += numBytesRead } + return nil } diff --git a/pkg/auxents/lecat.go b/pkg/auxents/lecat.go index 28cca62ba..71b8ecff7 100644 --- a/pkg/auxents/lecat.go +++ b/pkg/auxents/lecat.go @@ -7,14 +7,13 @@ import ( "os" ) -func lecatUsage(verbName string, o *os.File, exitCode int) { +func lecatUsage(verbName string, o *os.File) { fmt.Fprintf(o, "Usage: mlr %s [options] {zero or more file names}\n", verbName) fmt.Fprintf(o, "Simply echoes input, but flags CR characters in red and LF characters in green.\n") fmt.Fprintf(o, "If zero file names are supplied, standard input is read.\n") fmt.Fprintf(o, "Options:\n") fmt.Fprintf(o, "--mono: don't try to colorize the output\n") fmt.Fprintf(o, "-h or --help: print this message\n") - os.Exit(exitCode) } func lecatMain(args []string) int { @@ -25,7 +24,8 @@ func lecatMain(args []string) int { args = args[2:] if len(args) >= 1 { if args[0] == "-h" || args[0] == "--help" { - lecatUsage(verb, os.Stdout, 0) + lecatUsage(verb, os.Stdout) + return 0 } if args[0][0] == '-' { @@ -36,37 +36,46 @@ func lecatMain(args []string) int { fmt.Fprintf(os.Stderr, "mlr %s: unrecognized option \"%s\".\n", verb, args[0], ) - os.Exit(1) + return 1 } } } if len(args) == 0 { - lecatFile(os.Stdin, doColor) + if err := lecatFile(os.Stdin, doColor); err != nil { + fmt.Fprintf(os.Stderr, "mlr lecat: %v\n", err) + return 1 + } } else { for _, filename := range args { istream, err := os.Open(filename) if err != nil { fmt.Fprintf(os.Stderr, "mlr lecat: %v\n", err) - os.Exit(1) + return 1 } - lecatFile(istream, doColor) - + err = lecatFile(istream, doColor) _ = istream.Close() + if err != nil { + fmt.Fprintf(os.Stderr, "mlr lecat: %v\n", err) + return 1 + } } } return 0 } -func lecatFile(istream *os.File, doColor bool) { +func lecatFile(istream *os.File, doColor bool) error { reader := bufio.NewReader(istream) for { c, err := reader.ReadByte() if err == io.EOF { break } + if err != nil { + return err + } switch c { case '\r': if doColor { @@ -88,4 +97,5 @@ func lecatFile(istream *os.File, doColor bool) { fmt.Printf("%c", c) } } + return nil } diff --git a/pkg/auxents/termcvt.go b/pkg/auxents/termcvt.go index ad853e02a..33ee5be7f 100644 --- a/pkg/auxents/termcvt.go +++ b/pkg/auxents/termcvt.go @@ -8,7 +8,7 @@ import ( "strings" ) -func termcvtUsage(verbName string, o *os.File, exitCode int) { +func termcvtUsage(verbName string, o *os.File) { fmt.Fprintf(o, "Usage: mlr %s [option] {zero or more file names}\n", verbName) fmt.Fprintf(o, "Option (exactly one is required):\n") fmt.Fprintf(o, "--cr2crlf\n") @@ -21,7 +21,6 @@ func termcvtUsage(verbName string, o *os.File, exitCode int) { fmt.Fprintf(o, "-h or --help: print this message\n") fmt.Fprintf(o, "Zero file names means read from standard input.\n") fmt.Fprintf(o, "Output is always to standard output; files are not written in-place.\n") - os.Exit(exitCode) } func termcvtMain(args []string) int { @@ -33,7 +32,8 @@ func termcvtMain(args []string) int { verb := args[1] args = args[2:] if len(args) < 1 { - termcvtUsage(verb, os.Stderr, 1) + termcvtUsage(verb, os.Stderr) + return 1 } for len(args) >= 1 { @@ -45,7 +45,8 @@ func termcvtMain(args []string) int { switch opt { case "-h", "--help": - termcvtUsage(verb, os.Stdout, 0) + termcvtUsage(verb, os.Stdout) + return 0 case "-I": doInPlace = true case "--cr2crlf": @@ -67,12 +68,16 @@ func termcvtMain(args []string) int { inputTerminator = "\r" outputTerminator = "\n" default: - termcvtUsage(verb, os.Stderr, 1) + termcvtUsage(verb, os.Stderr) + return 1 } } if len(args) == 0 { - termcvtFile(os.Stdin, os.Stdout, inputTerminator, outputTerminator) + if err := termcvtFile(os.Stdin, os.Stdout, inputTerminator, outputTerminator); err != nil { + fmt.Fprintf(os.Stderr, "mlr termcvt: %v\n", err) + return 1 + } } else if doInPlace { for _, filename := range args { @@ -82,31 +87,31 @@ func termcvtMain(args []string) int { istream, err := os.Open(filename) if err != nil { - // TODO: "mlr" fmt.Fprintf(os.Stderr, "mlr termcvt: %v\n", err) - os.Exit(1) + return 1 } ostream, err := os.Open(tempname) if err != nil { - // TODO: "mlr" fmt.Fprintf(os.Stderr, "mlr termcvt: %v\n", err) - os.Exit(1) + return 1 } - termcvtFile(istream, ostream, inputTerminator, outputTerminator) + if err := termcvtFile(istream, ostream, inputTerminator, outputTerminator); err != nil { + fmt.Fprintf(os.Stderr, "mlr termcvt: %v\n", err) + return 1 + } _ = istream.Close() if err := ostream.Close(); err != nil { fmt.Fprintf(os.Stderr, "mlr termcvt: %v\n", err) - os.Exit(1) + return 1 } err = os.Rename(tempname, filename) if err != nil { - // TODO: "mlr" fmt.Fprintf(os.Stderr, "mlr termcvt: %v\n", err) - os.Exit(1) + return 1 } } @@ -115,20 +120,22 @@ func termcvtMain(args []string) int { istream, err := os.Open(filename) if err != nil { - // TODO: "mlr" fmt.Fprintf(os.Stderr, "mlr termcvt: %v\n", err) - os.Exit(1) + return 1 } - termcvtFile(istream, os.Stdout, inputTerminator, outputTerminator) - + err = termcvtFile(istream, os.Stdout, inputTerminator, outputTerminator) _ = istream.Close() + if err != nil { + fmt.Fprintf(os.Stderr, "mlr termcvt: %v\n", err) + return 1 + } } } return 0 } -func termcvtFile(istream *os.File, ostream *os.File, inputTerminator string, outputTerminator string) { +func termcvtFile(istream *os.File, ostream *os.File, inputTerminator string, outputTerminator string) error { lineReader := bufio.NewReader(istream) inputTerminatorBytes := []byte(inputTerminator[len(inputTerminator)-1:])[0] // bufio.Reader.ReadString takes char not string delimiter :( @@ -139,16 +146,14 @@ func termcvtFile(istream *os.File, ostream *os.File, inputTerminator string, out } if err != nil { - // TODO: "mlr" - fmt.Fprintf(os.Stderr, "mlr termcvt: %v\n", err) - os.Exit(1) + return err } // This is how to do a chomp: line = strings.TrimRight(line, inputTerminator) if _, err := ostream.Write([]byte(line + outputTerminator)); err != nil { - fmt.Fprintf(os.Stderr, "mlr termcvt: %v\n", err) - os.Exit(1) + return err } } + return nil } diff --git a/pkg/auxents/unhex.go b/pkg/auxents/unhex.go index 8e74e1b66..89f93d02a 100644 --- a/pkg/auxents/unhex.go +++ b/pkg/auxents/unhex.go @@ -2,6 +2,7 @@ package auxents import ( "bufio" + "errors" "fmt" "io" "os" @@ -9,13 +10,12 @@ import ( "strings" ) -func unhexUsage(verbName string, o *os.File, exitCode int) { +func unhexUsage(verbName string, o *os.File) { fmt.Fprintf(o, "Usage: mlr %s [option] {zero or more file names}\n", verbName) fmt.Fprintf(o, "Options:\n") fmt.Fprintf(o, "-h or --help: print this message\n") fmt.Fprintf(o, "Zero file names means read from standard input.\n") fmt.Fprintf(o, "Output is always to standard output; files are not written in-place.\n") - os.Exit(exitCode) } func unhexMain(args []string) int { @@ -25,28 +25,36 @@ func unhexMain(args []string) int { if len(args) >= 1 { if args[0] == "-h" || args[0] == "--help" { - unhexUsage(verb, os.Stdout, 0) + unhexUsage(verb, os.Stdout) + return 0 } } if len(args) == 0 { - unhexFile(os.Stdin, os.Stdout) + if err := unhexFile(os.Stdin, os.Stdout); err != nil { + fmt.Fprintf(os.Stderr, "mlr unhex: %v\n", err) + return 1 + } } else { for _, filename := range args { istream, err := os.Open(filename) if err != nil { fmt.Fprintf(os.Stderr, "mlr unhex: %v\n", err) - os.Exit(1) + return 1 } - unhexFile(istream, os.Stdout) + err = unhexFile(istream, os.Stdout) _ = istream.Close() + if err != nil { + fmt.Fprintf(os.Stderr, "mlr unhex: %v\n", err) + return 1 + } } } return 0 } -func unhexFile(istream *os.File, ostream *os.File) { +func unhexFile(istream *os.File, ostream *os.File) error { // Key insight is os.File implements io.Reader lineReader := bufio.NewReader(istream) @@ -62,8 +70,7 @@ func unhexFile(istream *os.File, ostream *os.File) { break } if err != nil { - fmt.Fprintf(os.Stderr, "mlr unhex: %v\n", err) - os.Exit(1) + return err } // This is how to do a chomp: @@ -75,19 +82,17 @@ func unhexFile(istream *os.File, ostream *os.File) { if field != "" { n, err := fmt.Sscanf(field, "%x", &scanValue) if err != nil { - fmt.Fprintf(os.Stderr, "mlr unhex: %v\n", err) - os.Exit(1) + return err } if n != 1 { - fmt.Fprintln(os.Stderr, "mlr unhex: internal coding error") - os.Exit(1) + return errors.New("internal coding error") } byteArray[0] = byte(scanValue) if _, err := ostream.Write(byteArray); err != nil { - fmt.Fprintln(os.Stderr, "mlr unhex:", err) - os.Exit(1) + return err } } } } + return nil } diff --git a/pkg/climain/mlrcli_parse.go b/pkg/climain/mlrcli_parse.go index 388686143..ee9ad38ef 100644 --- a/pkg/climain/mlrcli_parse.go +++ b/pkg/climain/mlrcli_parse.go @@ -396,9 +396,9 @@ func parseCommandLinePassTwo( } if terminalSequence != nil { - terminals.Dispatch(terminalSequence) - // They are expected to exit the process - panic("mlr: internal coding error: terminal did not exit the process") + // The terminal (help, repl, regtest, ...) runs here; its exit code + // rides an ExitRequest up to the entrypoint layer. + return nil, nil, &lib.ExitRequest{Code: terminals.Dispatch(terminalSequence)} } // Now process the verb-sequences from pass one, with options-struct set up diff --git a/pkg/entrypoint/entrypoint.go b/pkg/entrypoint/entrypoint.go index 8e6f44031..6d74d5ed2 100644 --- a/pkg/entrypoint/entrypoint.go +++ b/pkg/entrypoint/entrypoint.go @@ -38,8 +38,10 @@ func Main() MainReturn { // 'mlr repl' or 'mlr lecat' or any other non-miller-per-se toolery which // is delivered (for convenience) within the mlr executable. If argv[1] is - // found then this function will not return. - auxents.Dispatch(os.Args) + // an auxent name, it's run here and its exit code is the process's. + if handled, exitCode := auxents.Dispatch(os.Args); handled { + os.Exit(exitCode) + } if lib.IsTruthyEnvValue(os.Getenv("MLR_NO_SHELL")) { lib.DisableShellOut() diff --git a/pkg/terminals/regtest/entry.go b/pkg/terminals/regtest/entry.go index bc38e557c..f37abb807 100644 --- a/pkg/terminals/regtest/entry.go +++ b/pkg/terminals/regtest/entry.go @@ -11,7 +11,7 @@ import ( const defaultPath = "./test/cases" -func regTestUsage(verbName string, o *os.File, exitCode int) { +func regTestUsage(verbName string, o *os.File) { fmt.Fprintf(o, "Usage: mlr %s [options] [one or more directories/files]\n", verbName) fmt.Fprintf(o, "If no directories/files are specified, the directory %s is used by default.\n", defaultPath) fmt.Fprintf(o, "Recursively walks the directory/ies looking for foo.cmd files having Miller command-lines,\n") @@ -30,7 +30,6 @@ func regTestUsage(verbName string, o *os.File, exitCode int) { fmt.Fprintf(o, "-j Just show the Miller command-line, put/filter script if any, and output.\n") fmt.Fprintf(o, "-s {n} After running tests, re-run first n failed .cmd files with verbosity level 3.\n") fmt.Fprintf(o, "-S After running tests, re-run all failed .cmd files with verbosity level 3.\n") - os.Exit(exitCode) } // Here the args are the full Miller command line: "mlr regtest --foo bar". @@ -55,22 +54,26 @@ func RegTestMain(args []string) int { switch arg { case "-h", "--help": - regTestUsage(verbName, os.Stdout, 0) + regTestUsage(verbName, os.Stdout) + return 0 case "-m": if argi >= argc { - regTestUsage(verbName, os.Stderr, 1) + regTestUsage(verbName, os.Stderr) + return 1 } exeName = args[argi] argi++ case "-s": if argi >= argc { - regTestUsage(verbName, os.Stderr, 1) + regTestUsage(verbName, os.Stderr) + return 1 } temp, err := strconv.Atoi(args[argi]) if err != nil { - regTestUsage(verbName, os.Stderr, 1) + regTestUsage(verbName, os.Stderr) + return 1 } firstNFailsToShow = temp argi++ @@ -88,7 +91,8 @@ func RegTestMain(args []string) int { plainMode = true default: - regTestUsage(verbName, os.Stderr, 1) + regTestUsage(verbName, os.Stderr) + return 1 } } casePaths := args[argi:] @@ -104,7 +108,11 @@ func RegTestMain(args []string) int { firstNFailsToShow, ) - ok := regtester.Execute(casePaths) + ok, err := regtester.Execute(casePaths) + if err != nil { + fmt.Fprintf(os.Stderr, "mlr %s: %v\n", verbName, err) + return 1 + } if !ok { return 1 diff --git a/pkg/terminals/regtest/regtester.go b/pkg/terminals/regtest/regtester.go index 60244f4fa..d51acb7ca 100644 --- a/pkg/terminals/regtest/regtester.go +++ b/pkg/terminals/regtest/regtester.go @@ -146,7 +146,7 @@ func (rt *RegTester) resetCounts() { func (rt *RegTester) Execute( casePaths []string, -) bool { +) (bool, error) { // Don't let the current user's settings affect expected results for _, name := range envVarsToUnset { _ = os.Unsetenv(name) @@ -175,7 +175,9 @@ func (rt *RegTester) Execute( } for _, path := range casePaths { - rt.executeSinglePath(path) + if _, err := rt.executeSinglePath(path); err != nil { + return false, err + } } if len(rt.failCaseNames) > 0 && rt.firstNFailsToShow > 0 { @@ -215,12 +217,12 @@ func (rt *RegTester) Execute( if !rt.plainMode { fmt.Printf("%s overall\n", colorizer.MaybeColorizePass("PASS", true)) } - return true + return true, nil } if !rt.plainMode { fmt.Printf("%s overall\n", colorizer.MaybeColorizeFail("FAIL", true)) } - return false + return false, nil } // Recursively invoked routine to process either a single .cmd file, or a @@ -228,15 +230,18 @@ func (rt *RegTester) Execute( func (rt *RegTester) executeSinglePath( path string, -) bool { +) (bool, error) { handle, err := os.Stat(path) if err != nil { fmt.Printf("%s: %v\n", path, err) - return false + return false, nil } mode := handle.Mode() if mode.IsDir() { - passed, hasCaseSubdirectories := rt.executeSingleDirectory(path) + passed, hasCaseSubdirectories, err := rt.executeSingleDirectory(path) + if err != nil { + return false, err + } if hasCaseSubdirectories { if passed { rt.directoryPassCount++ @@ -245,7 +250,7 @@ func (rt *RegTester) executeSinglePath( rt.failDirNames = append(rt.failDirNames, path) } } - return passed + return passed, nil } else if mode.IsRegular() { basename := filepath.Base(path) if basename == CmdName { @@ -256,21 +261,24 @@ func (rt *RegTester) executeSinglePath( rt.caseFailCount++ rt.failCaseNames = append(rt.failCaseNames, path) } - return passed + return passed, nil } - return true // No .cmd files directly inside + return true, nil // No .cmd files directly inside } fmt.Printf("%s: neither directory nor regular file.\n", path) - return false // fall-through + return false, nil // fall-through } func (rt *RegTester) executeSingleDirectory( dirName string, -) (bool, bool) { +) (bool, bool, error) { passed := true // TODO: comment - fileNames, hasCaseSubdirectories := rt.hasCaseSubdirectories(dirName) + fileNames, hasCaseSubdirectories, err := rt.hasCaseSubdirectories(dirName) + if err != nil { + return false, false, err + } if !rt.plainMode { if hasCaseSubdirectories && rt.verbosityLevel >= 2 { @@ -281,7 +289,10 @@ func (rt *RegTester) executeSingleDirectory( for _, name := range fileNames { path := dirName + "/" + name - ok := rt.executeSinglePath(path) + ok, err := rt.executeSinglePath(path) + if err != nil { + return false, false, err + } if !ok { passed = false } @@ -309,7 +320,7 @@ func (rt *RegTester) executeSingleDirectory( } } - return passed, hasCaseSubdirectories + return passed, hasCaseSubdirectories, nil } // Sees if a directory contains a single test case. If so, we don't want to @@ -322,28 +333,26 @@ func (rt *RegTester) executeSingleDirectory( func (rt *RegTester) hasCaseSubdirectories( dirName string, -) ([]string, bool) { +) ([]string, bool, error) { f, err := os.Open(dirName) if err != nil { - fmt.Printf("%s: %v\n", dirName, err) - os.Exit(1) + return nil, false, err } defer func() { _ = f.Close() }() names, err := f.Readdirnames(-1) if err != nil { - fmt.Printf("%s: %v\n", dirName, err) - os.Exit(1) + return nil, false, err } sort.Strings(names) for _, name := range names { path := dirName + string(filepath.Separator) + name if rt.isCaseDirectory(path) { - return names, true + return names, true, nil } } - return names, false + return names, false, nil } func (rt *RegTester) isCaseDirectory( diff --git a/pkg/terminals/repl/entry.go b/pkg/terminals/repl/entry.go index 2fd3edbf8..2ac6071bf 100644 --- a/pkg/terminals/repl/entry.go +++ b/pkg/terminals/repl/entry.go @@ -30,7 +30,7 @@ import ( "github.com/johnkerl/miller/v6/pkg/lib" ) -func replUsage(verbName string, o *os.File, exitCode int) { +func replUsage(verbName string, o *os.File) { exeName := path.Base(os.Args[0]) fmt.Fprintf(o, "Usage: %s %s [options] {zero or more data-file names}\n", exeName, verbName) @@ -72,8 +72,6 @@ Or any --icsv, --ojson, etc. reader/writer options as for the main Miller comman Any data-file names are opened just as if you had waited and typed :open {filenames} at the Miller REPL prompt. `) - - os.Exit(exitCode) } // Here the args are the full Miller command line: if the latter was "mlr @@ -97,7 +95,8 @@ func ReplMain(args []string) int { } if args[argi] == "-h" || args[argi] == "--help" { - replUsage(replName, os.Stdout, 0) + replUsage(replName, os.Stdout) + return 0 } else if args[argi] == "-q" { showStartupBanner = false @@ -124,14 +123,16 @@ func ReplMain(args []string) int { } else if args[argi] == "--load" { if argc-argi < 2 { - replUsage(replName, os.Stderr, 1) + replUsage(replName, os.Stderr) + return 1 } options.DSLPreloadFileNames = append(options.DSLPreloadFileNames, args[argi+1]) argi += 2 } else if args[argi] == "--mload" { if argc-argi < 2 { - replUsage(replName, os.Stderr, 1) + replUsage(replName, os.Stderr) + return 1 } argi += 1 for argi < argc && args[argi] != "--" { @@ -153,7 +154,8 @@ func ReplMain(args []string) int { } else if handled { } else { - replUsage(replName, os.Stderr, 1) + replUsage(replName, os.Stderr) + return 1 } } @@ -188,7 +190,7 @@ func ReplMain(args []string) int { ) if err != nil { fmt.Fprintf(os.Stderr, "mlr: %v\n", err) - os.Exit(1) + return 1 } filenames := args[argi:] @@ -199,18 +201,18 @@ func ReplMain(args []string) int { err = repl.handleSession(os.Stdin) if err != nil { fmt.Fprintf(os.Stderr, "mlr %s: %v\n", repl.replName, err) - os.Exit(1) + return 1 } err = repl.bufferedRecordOutputStream.Flush() if err != nil { fmt.Fprintf(os.Stderr, "mlr %s: %v\n", repl.replName, err) - os.Exit(1) + return 1 } err = repl.closeBufferedOutputStream() if err != nil { fmt.Fprintf(os.Stderr, "mlr %s: %v\n", repl.replName, err) - os.Exit(1) + return 1 } return 0 } diff --git a/pkg/terminals/script/entry.go b/pkg/terminals/script/entry.go index d25176ca6..06c1f04f2 100644 --- a/pkg/terminals/script/entry.go +++ b/pkg/terminals/script/entry.go @@ -20,7 +20,7 @@ import ( "github.com/johnkerl/miller/v6/pkg/lib" ) -func scriptUsage(verbName string, o *os.File, exitCode int) { +func scriptUsage(verbName string, o *os.File) { exeName := path.Base(os.Args[0]) fmt.Fprintf(o, "Usage: %s %s [options] -f {script file} | -e {expression} [zero or more data-file names]\n", exeName, verbName) fmt.Fprint(o, @@ -39,7 +39,6 @@ Or any --icsv, --ojson, etc. reader/writer options. Data-file names (or stdin if none) are the record input stream. `) - os.Exit(exitCode) } func ScriptMain(args []string) int { @@ -59,7 +58,8 @@ func ScriptMain(args []string) int { } if args[argi] == "-h" || args[argi] == "--help" { - scriptUsage(scriptName, os.Stdout, 0) + scriptUsage(scriptName, os.Stdout) + return 0 } else if args[argi] == "-w" { doWarnings = true argi++ @@ -68,42 +68,46 @@ func ScriptMain(args []string) int { argi++ } else if args[argi] == "-f" { if argc-argi < 2 { - scriptUsage(scriptName, os.Stderr, 1) + scriptUsage(scriptName, os.Stderr) + return 1 } argi++ filename, err := cli.VerbGetStringArg("script", "-f", args, &argi, argc) if err != nil { fmt.Fprintf(os.Stderr, "mlr: %v\n", err) - os.Exit(1) + return 1 } theseStrings, err := lib.LoadStringsFromFileOrDir(filename, ".mlr") if err != nil { fmt.Fprintf(os.Stderr, "mlr script: cannot load script from \"%s\": %v\n", filename, err) - os.Exit(1) + return 1 } dslStrings = append(dslStrings, theseStrings...) haveDSLStrings = true } else if args[argi] == "-e" { if argc-argi < 2 { - scriptUsage(scriptName, os.Stderr, 1) + scriptUsage(scriptName, os.Stderr) + return 1 } argi++ expr, err := cli.VerbGetStringArg("script", "-e", args, &argi, argc) if err != nil { fmt.Fprintf(os.Stderr, "mlr: %v\n", err) - os.Exit(1) + return 1 } dslStrings = append(dslStrings, expr) haveDSLStrings = true } else if args[argi] == "--load" { if argc-argi < 2 { - scriptUsage(scriptName, os.Stderr, 1) + scriptUsage(scriptName, os.Stderr) + return 1 } options.DSLPreloadFileNames = append(options.DSLPreloadFileNames, args[argi+1]) argi += 2 } else if args[argi] == "--mload" { if argc-argi < 2 { - scriptUsage(scriptName, os.Stderr, 1) + scriptUsage(scriptName, os.Stderr) + return 1 } argi++ for argi < argc && args[argi] != "--" { @@ -122,13 +126,15 @@ func ScriptMain(args []string) int { return 1 } else if handled { } else { - scriptUsage(scriptName, os.Stderr, 1) + scriptUsage(scriptName, os.Stderr) + return 1 } } if !haveDSLStrings { fmt.Fprintf(os.Stderr, "mlr script: -f or -e is required\n") - scriptUsage(scriptName, os.Stderr, 1) + scriptUsage(scriptName, os.Stderr) + return 1 } if err := cli.FinalizeReaderOptions(&options.ReaderOptions); err != nil { @@ -147,25 +153,25 @@ func ScriptMain(args []string) int { scr, err := NewScript(options, doWarnings, strictMode, dslStrings) if err != nil { fmt.Fprintf(os.Stderr, "mlr script: %v\n", err) - os.Exit(1) + return 1 } scr.openFiles(filenames) err = scr.run() if err != nil { fmt.Fprintf(os.Stderr, "mlr script: %v\n", err) - os.Exit(1) + return 1 } err = scr.bufferedRecordOutputStream.Flush() if err != nil { fmt.Fprintf(os.Stderr, "mlr script: %v\n", err) - os.Exit(1) + return 1 } err = scr.closeBufferedOutputStream() if err != nil { fmt.Fprintf(os.Stderr, "mlr script: %v\n", err) - os.Exit(1) + return 1 } return 0 } diff --git a/pkg/terminals/terminals.go b/pkg/terminals/terminals.go index 0df047acc..4f6ac8f08 100644 --- a/pkg/terminals/terminals.go +++ b/pkg/terminals/terminals.go @@ -57,19 +57,24 @@ func Dispatchable(arg string) bool { return false } -func Dispatch(args []string) { +// Dispatch runs the terminal named by args[0] and returns the process exit +// code for the caller to propagate (via lib.ExitRequest) up to the entrypoint +// layer. +func Dispatch(args []string) int { if len(args) < 1 { - return + // Can't happen: the climain caller passes a non-empty terminal sequence. + fmt.Fprintf(os.Stderr, "mlr: internal coding error: empty terminal sequence.\n") + return 1 } terminal := args[0] for _, entry := range _TERMINAL_LOOKUP_TABLE { if terminal == entry.name { - os.Exit(entry.main(args)) + return entry.main(args) } } fmt.Fprintf(os.Stderr, "mlr: terminal \"%s\" not found.\n", terminal) - os.Exit(1) + return 1 } // terminalListMain is the handler for 'mlr terminal-list'. diff --git a/regression_test.go b/regression_test.go index 0ccaa3a15..58f59da13 100644 --- a/regression_test.go +++ b/regression_test.go @@ -65,7 +65,10 @@ func TestRegression(t *testing.T) { firstNFailsToShow, ) - ok := regtester.Execute(casePaths) + ok, err := regtester.Execute(casePaths) + if err != nil { + t.Fatal(err) + } if !ok { t.Fatal() }