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 <noreply@anthropic.com>
This commit is contained in:
John Kerl 2026-07-15 15:27:26 -04:00 committed by GitHub
parent 314b26cfaa
commit 2f3e54d1f6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 195 additions and 133 deletions

View file

@ -32,22 +32,23 @@ func init() {
} }
} }
// Dispatch is called from Miller main. Here we indicate if argv[1] is handled // Dispatch is called from Miller main. The boolean return says whether
// by us, or not. If so, we handle it and exit, not returning control to Miller // argv[1] named an auxent handled here; if so, the int is the process exit
// main. // code for the entrypoint layer to exit with. Otherwise control returns to
func Dispatch(args []string) { // main for the rest of Miller.
func Dispatch(args []string) (bool, int) {
if len(args) < 2 { if len(args) < 2 {
return return false, 0
} }
verb := args[1] verb := args[1]
for _, entry := range _AUX_LOOKUP_TABLE { for _, entry := range _AUX_LOOKUP_TABLE {
if verb == entry.name { 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'. // auxListMain is the handler for 'mlr aux-list'.

View file

@ -23,14 +23,13 @@ import (
// 00000060: 60 61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f |`abcdefghijklmno| // 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{|}~.| // 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, "Usage: mlr %s [options] {zero or more file names}\n", verbName)
fmt.Fprintf(o, "Simple hex-dump.\n") fmt.Fprintf(o, "Simple hex-dump.\n")
fmt.Fprintf(o, "If zero file names are supplied, standard input is read.\n") fmt.Fprintf(o, "If zero file names are supplied, standard input is read.\n")
fmt.Fprintf(o, "Options:\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, "-r: print only raw hex without leading offset indicators or trailing ASCII dump.\n")
fmt.Fprintf(o, "-h or --help: print this message\n") fmt.Fprintf(o, "-h or --help: print this message\n")
os.Exit(exitCode)
} }
func hexMain(args []string) int { func hexMain(args []string) int {
@ -45,12 +44,16 @@ func hexMain(args []string) int {
doRaw = true doRaw = true
args = args[1:] args = args[1:]
case "-h", "--help": case "-h", "--help":
hexUsage(verb, os.Stdout, 0) hexUsage(verb, os.Stdout)
return 0
} }
} }
if len(args) == 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 { } else {
for _, filename := range args { for _, filename := range args {
// Print filename if there is more than one file, unless raw output // 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) istream, err := os.Open(filename)
if err != nil { if err != nil {
// TODO: "mlr"
fmt.Fprintf(os.Stderr, "mlr hex: %v\n", err) fmt.Fprintf(os.Stderr, "mlr hex: %v\n", err)
os.Exit(1) return 1
} }
hexDumpFile(istream, doRaw) err = hexDumpFile(istream, doRaw)
_ = istream.Close() _ = istream.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "mlr hex: %v\n", err)
return 1
}
if !doRaw && len(args) > 1 { if !doRaw && len(args) > 1 {
fmt.Println() fmt.Println()
} }
@ -78,7 +84,7 @@ func hexMain(args []string) int {
return 0 return 0
} }
func hexDumpFile(istream *os.File, doRaw bool) { func hexDumpFile(istream *os.File, doRaw bool) error {
const bytesPerClump = 4 const bytesPerClump = 4
const clumpsPerLine = 4 const clumpsPerLine = 4
const bufferSize = bytesPerClump * clumpsPerLine 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 // exact multiple of our buffer size. We'll break the loop after
// hex-dumping this last, partial fragment. // hex-dumping this last, partial fragment.
if err != nil && err != io.ErrUnexpectedEOF { if err != nil && err != io.ErrUnexpectedEOF {
fmt.Fprintf(os.Stderr, "mlr hex: %v\n", err) return err
os.Exit(1)
} }
// Print offset "pre" part // Print offset "pre" part
@ -148,4 +153,5 @@ func hexDumpFile(istream *os.File, doRaw bool) {
offset += numBytesRead offset += numBytesRead
} }
return nil
} }

View file

@ -7,14 +7,13 @@ import (
"os" "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, "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, "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, "If zero file names are supplied, standard input is read.\n")
fmt.Fprintf(o, "Options:\n") fmt.Fprintf(o, "Options:\n")
fmt.Fprintf(o, "--mono: don't try to colorize the output\n") fmt.Fprintf(o, "--mono: don't try to colorize the output\n")
fmt.Fprintf(o, "-h or --help: print this message\n") fmt.Fprintf(o, "-h or --help: print this message\n")
os.Exit(exitCode)
} }
func lecatMain(args []string) int { func lecatMain(args []string) int {
@ -25,7 +24,8 @@ func lecatMain(args []string) int {
args = args[2:] args = args[2:]
if len(args) >= 1 { if len(args) >= 1 {
if args[0] == "-h" || args[0] == "--help" { if args[0] == "-h" || args[0] == "--help" {
lecatUsage(verb, os.Stdout, 0) lecatUsage(verb, os.Stdout)
return 0
} }
if args[0][0] == '-' { if args[0][0] == '-' {
@ -36,37 +36,46 @@ func lecatMain(args []string) int {
fmt.Fprintf(os.Stderr, "mlr %s: unrecognized option \"%s\".\n", fmt.Fprintf(os.Stderr, "mlr %s: unrecognized option \"%s\".\n",
verb, args[0], verb, args[0],
) )
os.Exit(1) return 1
} }
} }
} }
if len(args) == 0 { 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 { } else {
for _, filename := range args { for _, filename := range args {
istream, err := os.Open(filename) istream, err := os.Open(filename)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "mlr lecat: %v\n", err) fmt.Fprintf(os.Stderr, "mlr lecat: %v\n", err)
os.Exit(1) return 1
} }
lecatFile(istream, doColor) err = lecatFile(istream, doColor)
_ = istream.Close() _ = istream.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "mlr lecat: %v\n", err)
return 1
}
} }
} }
return 0 return 0
} }
func lecatFile(istream *os.File, doColor bool) { func lecatFile(istream *os.File, doColor bool) error {
reader := bufio.NewReader(istream) reader := bufio.NewReader(istream)
for { for {
c, err := reader.ReadByte() c, err := reader.ReadByte()
if err == io.EOF { if err == io.EOF {
break break
} }
if err != nil {
return err
}
switch c { switch c {
case '\r': case '\r':
if doColor { if doColor {
@ -88,4 +97,5 @@ func lecatFile(istream *os.File, doColor bool) {
fmt.Printf("%c", c) fmt.Printf("%c", c)
} }
} }
return nil
} }

View file

@ -8,7 +8,7 @@ import (
"strings" "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, "Usage: mlr %s [option] {zero or more file names}\n", verbName)
fmt.Fprintf(o, "Option (exactly one is required):\n") fmt.Fprintf(o, "Option (exactly one is required):\n")
fmt.Fprintf(o, "--cr2crlf\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, "-h or --help: print this message\n")
fmt.Fprintf(o, "Zero file names means read from standard input.\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") fmt.Fprintf(o, "Output is always to standard output; files are not written in-place.\n")
os.Exit(exitCode)
} }
func termcvtMain(args []string) int { func termcvtMain(args []string) int {
@ -33,7 +32,8 @@ func termcvtMain(args []string) int {
verb := args[1] verb := args[1]
args = args[2:] args = args[2:]
if len(args) < 1 { if len(args) < 1 {
termcvtUsage(verb, os.Stderr, 1) termcvtUsage(verb, os.Stderr)
return 1
} }
for len(args) >= 1 { for len(args) >= 1 {
@ -45,7 +45,8 @@ func termcvtMain(args []string) int {
switch opt { switch opt {
case "-h", "--help": case "-h", "--help":
termcvtUsage(verb, os.Stdout, 0) termcvtUsage(verb, os.Stdout)
return 0
case "-I": case "-I":
doInPlace = true doInPlace = true
case "--cr2crlf": case "--cr2crlf":
@ -67,12 +68,16 @@ func termcvtMain(args []string) int {
inputTerminator = "\r" inputTerminator = "\r"
outputTerminator = "\n" outputTerminator = "\n"
default: default:
termcvtUsage(verb, os.Stderr, 1) termcvtUsage(verb, os.Stderr)
return 1
} }
} }
if len(args) == 0 { 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 { } else if doInPlace {
for _, filename := range args { for _, filename := range args {
@ -82,31 +87,31 @@ func termcvtMain(args []string) int {
istream, err := os.Open(filename) istream, err := os.Open(filename)
if err != nil { if err != nil {
// TODO: "mlr"
fmt.Fprintf(os.Stderr, "mlr termcvt: %v\n", err) fmt.Fprintf(os.Stderr, "mlr termcvt: %v\n", err)
os.Exit(1) return 1
} }
ostream, err := os.Open(tempname) ostream, err := os.Open(tempname)
if err != nil { if err != nil {
// TODO: "mlr"
fmt.Fprintf(os.Stderr, "mlr termcvt: %v\n", err) 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() _ = istream.Close()
if err := ostream.Close(); err != nil { if err := ostream.Close(); err != nil {
fmt.Fprintf(os.Stderr, "mlr termcvt: %v\n", err) fmt.Fprintf(os.Stderr, "mlr termcvt: %v\n", err)
os.Exit(1) return 1
} }
err = os.Rename(tempname, filename) err = os.Rename(tempname, filename)
if err != nil { if err != nil {
// TODO: "mlr"
fmt.Fprintf(os.Stderr, "mlr termcvt: %v\n", err) 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) istream, err := os.Open(filename)
if err != nil { if err != nil {
// TODO: "mlr"
fmt.Fprintf(os.Stderr, "mlr termcvt: %v\n", err) 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() _ = istream.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "mlr termcvt: %v\n", err)
return 1
}
} }
} }
return 0 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) lineReader := bufio.NewReader(istream)
inputTerminatorBytes := []byte(inputTerminator[len(inputTerminator)-1:])[0] // bufio.Reader.ReadString takes char not string delimiter :( 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 { if err != nil {
// TODO: "mlr" return err
fmt.Fprintf(os.Stderr, "mlr termcvt: %v\n", err)
os.Exit(1)
} }
// This is how to do a chomp: // This is how to do a chomp:
line = strings.TrimRight(line, inputTerminator) line = strings.TrimRight(line, inputTerminator)
if _, err := ostream.Write([]byte(line + outputTerminator)); err != nil { if _, err := ostream.Write([]byte(line + outputTerminator)); err != nil {
fmt.Fprintf(os.Stderr, "mlr termcvt: %v\n", err) return err
os.Exit(1)
} }
} }
return nil
} }

View file

@ -2,6 +2,7 @@ package auxents
import ( import (
"bufio" "bufio"
"errors"
"fmt" "fmt"
"io" "io"
"os" "os"
@ -9,13 +10,12 @@ import (
"strings" "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, "Usage: mlr %s [option] {zero or more file names}\n", verbName)
fmt.Fprintf(o, "Options:\n") fmt.Fprintf(o, "Options:\n")
fmt.Fprintf(o, "-h or --help: print this message\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, "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") fmt.Fprintf(o, "Output is always to standard output; files are not written in-place.\n")
os.Exit(exitCode)
} }
func unhexMain(args []string) int { func unhexMain(args []string) int {
@ -25,28 +25,36 @@ func unhexMain(args []string) int {
if len(args) >= 1 { if len(args) >= 1 {
if args[0] == "-h" || args[0] == "--help" { if args[0] == "-h" || args[0] == "--help" {
unhexUsage(verb, os.Stdout, 0) unhexUsage(verb, os.Stdout)
return 0
} }
} }
if len(args) == 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 { } else {
for _, filename := range args { for _, filename := range args {
istream, err := os.Open(filename) istream, err := os.Open(filename)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "mlr unhex: %v\n", err) 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() _ = istream.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "mlr unhex: %v\n", err)
return 1
}
} }
} }
return 0 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 // Key insight is os.File implements io.Reader
lineReader := bufio.NewReader(istream) lineReader := bufio.NewReader(istream)
@ -62,8 +70,7 @@ func unhexFile(istream *os.File, ostream *os.File) {
break break
} }
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "mlr unhex: %v\n", err) return err
os.Exit(1)
} }
// This is how to do a chomp: // This is how to do a chomp:
@ -75,19 +82,17 @@ func unhexFile(istream *os.File, ostream *os.File) {
if field != "" { if field != "" {
n, err := fmt.Sscanf(field, "%x", &scanValue) n, err := fmt.Sscanf(field, "%x", &scanValue)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "mlr unhex: %v\n", err) return err
os.Exit(1)
} }
if n != 1 { if n != 1 {
fmt.Fprintln(os.Stderr, "mlr unhex: internal coding error") return errors.New("internal coding error")
os.Exit(1)
} }
byteArray[0] = byte(scanValue) byteArray[0] = byte(scanValue)
if _, err := ostream.Write(byteArray); err != nil { if _, err := ostream.Write(byteArray); err != nil {
fmt.Fprintln(os.Stderr, "mlr unhex:", err) return err
os.Exit(1)
} }
} }
} }
} }
return nil
} }

View file

@ -396,9 +396,9 @@ func parseCommandLinePassTwo(
} }
if terminalSequence != nil { if terminalSequence != nil {
terminals.Dispatch(terminalSequence) // The terminal (help, repl, regtest, ...) runs here; its exit code
// They are expected to exit the process // rides an ExitRequest up to the entrypoint layer.
panic("mlr: internal coding error: terminal did not exit the process") return nil, nil, &lib.ExitRequest{Code: terminals.Dispatch(terminalSequence)}
} }
// Now process the verb-sequences from pass one, with options-struct set up // Now process the verb-sequences from pass one, with options-struct set up

View file

@ -38,8 +38,10 @@ func Main() MainReturn {
// 'mlr repl' or 'mlr lecat' or any other non-miller-per-se toolery which // '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 // is delivered (for convenience) within the mlr executable. If argv[1] is
// found then this function will not return. // an auxent name, it's run here and its exit code is the process's.
auxents.Dispatch(os.Args) if handled, exitCode := auxents.Dispatch(os.Args); handled {
os.Exit(exitCode)
}
if lib.IsTruthyEnvValue(os.Getenv("MLR_NO_SHELL")) { if lib.IsTruthyEnvValue(os.Getenv("MLR_NO_SHELL")) {
lib.DisableShellOut() lib.DisableShellOut()

View file

@ -11,7 +11,7 @@ import (
const defaultPath = "./test/cases" 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, "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, "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") 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, "-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 {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") 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". // Here the args are the full Miller command line: "mlr regtest --foo bar".
@ -55,22 +54,26 @@ func RegTestMain(args []string) int {
switch arg { switch arg {
case "-h", "--help": case "-h", "--help":
regTestUsage(verbName, os.Stdout, 0) regTestUsage(verbName, os.Stdout)
return 0
case "-m": case "-m":
if argi >= argc { if argi >= argc {
regTestUsage(verbName, os.Stderr, 1) regTestUsage(verbName, os.Stderr)
return 1
} }
exeName = args[argi] exeName = args[argi]
argi++ argi++
case "-s": case "-s":
if argi >= argc { if argi >= argc {
regTestUsage(verbName, os.Stderr, 1) regTestUsage(verbName, os.Stderr)
return 1
} }
temp, err := strconv.Atoi(args[argi]) temp, err := strconv.Atoi(args[argi])
if err != nil { if err != nil {
regTestUsage(verbName, os.Stderr, 1) regTestUsage(verbName, os.Stderr)
return 1
} }
firstNFailsToShow = temp firstNFailsToShow = temp
argi++ argi++
@ -88,7 +91,8 @@ func RegTestMain(args []string) int {
plainMode = true plainMode = true
default: default:
regTestUsage(verbName, os.Stderr, 1) regTestUsage(verbName, os.Stderr)
return 1
} }
} }
casePaths := args[argi:] casePaths := args[argi:]
@ -104,7 +108,11 @@ func RegTestMain(args []string) int {
firstNFailsToShow, 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 { if !ok {
return 1 return 1

View file

@ -146,7 +146,7 @@ func (rt *RegTester) resetCounts() {
func (rt *RegTester) Execute( func (rt *RegTester) Execute(
casePaths []string, casePaths []string,
) bool { ) (bool, error) {
// Don't let the current user's settings affect expected results // Don't let the current user's settings affect expected results
for _, name := range envVarsToUnset { for _, name := range envVarsToUnset {
_ = os.Unsetenv(name) _ = os.Unsetenv(name)
@ -175,7 +175,9 @@ func (rt *RegTester) Execute(
} }
for _, path := range casePaths { 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 { if len(rt.failCaseNames) > 0 && rt.firstNFailsToShow > 0 {
@ -215,12 +217,12 @@ func (rt *RegTester) Execute(
if !rt.plainMode { if !rt.plainMode {
fmt.Printf("%s overall\n", colorizer.MaybeColorizePass("PASS", true)) fmt.Printf("%s overall\n", colorizer.MaybeColorizePass("PASS", true))
} }
return true return true, nil
} }
if !rt.plainMode { if !rt.plainMode {
fmt.Printf("%s overall\n", colorizer.MaybeColorizeFail("FAIL", true)) 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 // Recursively invoked routine to process either a single .cmd file, or a
@ -228,15 +230,18 @@ func (rt *RegTester) Execute(
func (rt *RegTester) executeSinglePath( func (rt *RegTester) executeSinglePath(
path string, path string,
) bool { ) (bool, error) {
handle, err := os.Stat(path) handle, err := os.Stat(path)
if err != nil { if err != nil {
fmt.Printf("%s: %v\n", path, err) fmt.Printf("%s: %v\n", path, err)
return false return false, nil
} }
mode := handle.Mode() mode := handle.Mode()
if mode.IsDir() { if mode.IsDir() {
passed, hasCaseSubdirectories := rt.executeSingleDirectory(path) passed, hasCaseSubdirectories, err := rt.executeSingleDirectory(path)
if err != nil {
return false, err
}
if hasCaseSubdirectories { if hasCaseSubdirectories {
if passed { if passed {
rt.directoryPassCount++ rt.directoryPassCount++
@ -245,7 +250,7 @@ func (rt *RegTester) executeSinglePath(
rt.failDirNames = append(rt.failDirNames, path) rt.failDirNames = append(rt.failDirNames, path)
} }
} }
return passed return passed, nil
} else if mode.IsRegular() { } else if mode.IsRegular() {
basename := filepath.Base(path) basename := filepath.Base(path)
if basename == CmdName { if basename == CmdName {
@ -256,21 +261,24 @@ func (rt *RegTester) executeSinglePath(
rt.caseFailCount++ rt.caseFailCount++
rt.failCaseNames = append(rt.failCaseNames, path) 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) fmt.Printf("%s: neither directory nor regular file.\n", path)
return false // fall-through return false, nil // fall-through
} }
func (rt *RegTester) executeSingleDirectory( func (rt *RegTester) executeSingleDirectory(
dirName string, dirName string,
) (bool, bool) { ) (bool, bool, error) {
passed := true passed := true
// TODO: comment // TODO: comment
fileNames, hasCaseSubdirectories := rt.hasCaseSubdirectories(dirName) fileNames, hasCaseSubdirectories, err := rt.hasCaseSubdirectories(dirName)
if err != nil {
return false, false, err
}
if !rt.plainMode { if !rt.plainMode {
if hasCaseSubdirectories && rt.verbosityLevel >= 2 { if hasCaseSubdirectories && rt.verbosityLevel >= 2 {
@ -281,7 +289,10 @@ func (rt *RegTester) executeSingleDirectory(
for _, name := range fileNames { for _, name := range fileNames {
path := dirName + "/" + name path := dirName + "/" + name
ok := rt.executeSinglePath(path) ok, err := rt.executeSinglePath(path)
if err != nil {
return false, false, err
}
if !ok { if !ok {
passed = false 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 // 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( func (rt *RegTester) hasCaseSubdirectories(
dirName string, dirName string,
) ([]string, bool) { ) ([]string, bool, error) {
f, err := os.Open(dirName) f, err := os.Open(dirName)
if err != nil { if err != nil {
fmt.Printf("%s: %v\n", dirName, err) return nil, false, err
os.Exit(1)
} }
defer func() { _ = f.Close() }() defer func() { _ = f.Close() }()
names, err := f.Readdirnames(-1) names, err := f.Readdirnames(-1)
if err != nil { if err != nil {
fmt.Printf("%s: %v\n", dirName, err) return nil, false, err
os.Exit(1)
} }
sort.Strings(names) sort.Strings(names)
for _, name := range names { for _, name := range names {
path := dirName + string(filepath.Separator) + name path := dirName + string(filepath.Separator) + name
if rt.isCaseDirectory(path) { if rt.isCaseDirectory(path) {
return names, true return names, true, nil
} }
} }
return names, false return names, false, nil
} }
func (rt *RegTester) isCaseDirectory( func (rt *RegTester) isCaseDirectory(

View file

@ -30,7 +30,7 @@ import (
"github.com/johnkerl/miller/v6/pkg/lib" "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]) exeName := path.Base(os.Args[0])
fmt.Fprintf(o, "Usage: %s %s [options] {zero or more data-file names}\n", exeName, verbName) 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} Any data-file names are opened just as if you had waited and typed :open {filenames}
at the Miller REPL prompt. at the Miller REPL prompt.
`) `)
os.Exit(exitCode)
} }
// Here the args are the full Miller command line: if the latter was "mlr // 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" { if args[argi] == "-h" || args[argi] == "--help" {
replUsage(replName, os.Stdout, 0) replUsage(replName, os.Stdout)
return 0
} else if args[argi] == "-q" { } else if args[argi] == "-q" {
showStartupBanner = false showStartupBanner = false
@ -124,14 +123,16 @@ func ReplMain(args []string) int {
} else if args[argi] == "--load" { } else if args[argi] == "--load" {
if argc-argi < 2 { if argc-argi < 2 {
replUsage(replName, os.Stderr, 1) replUsage(replName, os.Stderr)
return 1
} }
options.DSLPreloadFileNames = append(options.DSLPreloadFileNames, args[argi+1]) options.DSLPreloadFileNames = append(options.DSLPreloadFileNames, args[argi+1])
argi += 2 argi += 2
} else if args[argi] == "--mload" { } else if args[argi] == "--mload" {
if argc-argi < 2 { if argc-argi < 2 {
replUsage(replName, os.Stderr, 1) replUsage(replName, os.Stderr)
return 1
} }
argi += 1 argi += 1
for argi < argc && args[argi] != "--" { for argi < argc && args[argi] != "--" {
@ -153,7 +154,8 @@ func ReplMain(args []string) int {
} else if handled { } else if handled {
} else { } else {
replUsage(replName, os.Stderr, 1) replUsage(replName, os.Stderr)
return 1
} }
} }
@ -188,7 +190,7 @@ func ReplMain(args []string) int {
) )
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "mlr: %v\n", err) fmt.Fprintf(os.Stderr, "mlr: %v\n", err)
os.Exit(1) return 1
} }
filenames := args[argi:] filenames := args[argi:]
@ -199,18 +201,18 @@ func ReplMain(args []string) int {
err = repl.handleSession(os.Stdin) err = repl.handleSession(os.Stdin)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "mlr %s: %v\n", repl.replName, err) fmt.Fprintf(os.Stderr, "mlr %s: %v\n", repl.replName, err)
os.Exit(1) return 1
} }
err = repl.bufferedRecordOutputStream.Flush() err = repl.bufferedRecordOutputStream.Flush()
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "mlr %s: %v\n", repl.replName, err) fmt.Fprintf(os.Stderr, "mlr %s: %v\n", repl.replName, err)
os.Exit(1) return 1
} }
err = repl.closeBufferedOutputStream() err = repl.closeBufferedOutputStream()
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "mlr %s: %v\n", repl.replName, err) fmt.Fprintf(os.Stderr, "mlr %s: %v\n", repl.replName, err)
os.Exit(1) return 1
} }
return 0 return 0
} }

View file

@ -20,7 +20,7 @@ import (
"github.com/johnkerl/miller/v6/pkg/lib" "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]) 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.Fprintf(o, "Usage: %s %s [options] -f {script file} | -e {expression} [zero or more data-file names]\n", exeName, verbName)
fmt.Fprint(o, 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. Data-file names (or stdin if none) are the record input stream.
`) `)
os.Exit(exitCode)
} }
func ScriptMain(args []string) int { func ScriptMain(args []string) int {
@ -59,7 +58,8 @@ func ScriptMain(args []string) int {
} }
if args[argi] == "-h" || args[argi] == "--help" { if args[argi] == "-h" || args[argi] == "--help" {
scriptUsage(scriptName, os.Stdout, 0) scriptUsage(scriptName, os.Stdout)
return 0
} else if args[argi] == "-w" { } else if args[argi] == "-w" {
doWarnings = true doWarnings = true
argi++ argi++
@ -68,42 +68,46 @@ func ScriptMain(args []string) int {
argi++ argi++
} else if args[argi] == "-f" { } else if args[argi] == "-f" {
if argc-argi < 2 { if argc-argi < 2 {
scriptUsage(scriptName, os.Stderr, 1) scriptUsage(scriptName, os.Stderr)
return 1
} }
argi++ argi++
filename, err := cli.VerbGetStringArg("script", "-f", args, &argi, argc) filename, err := cli.VerbGetStringArg("script", "-f", args, &argi, argc)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "mlr: %v\n", err) fmt.Fprintf(os.Stderr, "mlr: %v\n", err)
os.Exit(1) return 1
} }
theseStrings, err := lib.LoadStringsFromFileOrDir(filename, ".mlr") theseStrings, err := lib.LoadStringsFromFileOrDir(filename, ".mlr")
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "mlr script: cannot load script from \"%s\": %v\n", filename, err) fmt.Fprintf(os.Stderr, "mlr script: cannot load script from \"%s\": %v\n", filename, err)
os.Exit(1) return 1
} }
dslStrings = append(dslStrings, theseStrings...) dslStrings = append(dslStrings, theseStrings...)
haveDSLStrings = true haveDSLStrings = true
} else if args[argi] == "-e" { } else if args[argi] == "-e" {
if argc-argi < 2 { if argc-argi < 2 {
scriptUsage(scriptName, os.Stderr, 1) scriptUsage(scriptName, os.Stderr)
return 1
} }
argi++ argi++
expr, err := cli.VerbGetStringArg("script", "-e", args, &argi, argc) expr, err := cli.VerbGetStringArg("script", "-e", args, &argi, argc)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "mlr: %v\n", err) fmt.Fprintf(os.Stderr, "mlr: %v\n", err)
os.Exit(1) return 1
} }
dslStrings = append(dslStrings, expr) dslStrings = append(dslStrings, expr)
haveDSLStrings = true haveDSLStrings = true
} else if args[argi] == "--load" { } else if args[argi] == "--load" {
if argc-argi < 2 { if argc-argi < 2 {
scriptUsage(scriptName, os.Stderr, 1) scriptUsage(scriptName, os.Stderr)
return 1
} }
options.DSLPreloadFileNames = append(options.DSLPreloadFileNames, args[argi+1]) options.DSLPreloadFileNames = append(options.DSLPreloadFileNames, args[argi+1])
argi += 2 argi += 2
} else if args[argi] == "--mload" { } else if args[argi] == "--mload" {
if argc-argi < 2 { if argc-argi < 2 {
scriptUsage(scriptName, os.Stderr, 1) scriptUsage(scriptName, os.Stderr)
return 1
} }
argi++ argi++
for argi < argc && args[argi] != "--" { for argi < argc && args[argi] != "--" {
@ -122,13 +126,15 @@ func ScriptMain(args []string) int {
return 1 return 1
} else if handled { } else if handled {
} else { } else {
scriptUsage(scriptName, os.Stderr, 1) scriptUsage(scriptName, os.Stderr)
return 1
} }
} }
if !haveDSLStrings { if !haveDSLStrings {
fmt.Fprintf(os.Stderr, "mlr script: -f or -e is required\n") 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 { if err := cli.FinalizeReaderOptions(&options.ReaderOptions); err != nil {
@ -147,25 +153,25 @@ func ScriptMain(args []string) int {
scr, err := NewScript(options, doWarnings, strictMode, dslStrings) scr, err := NewScript(options, doWarnings, strictMode, dslStrings)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "mlr script: %v\n", err) fmt.Fprintf(os.Stderr, "mlr script: %v\n", err)
os.Exit(1) return 1
} }
scr.openFiles(filenames) scr.openFiles(filenames)
err = scr.run() err = scr.run()
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "mlr script: %v\n", err) fmt.Fprintf(os.Stderr, "mlr script: %v\n", err)
os.Exit(1) return 1
} }
err = scr.bufferedRecordOutputStream.Flush() err = scr.bufferedRecordOutputStream.Flush()
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "mlr script: %v\n", err) fmt.Fprintf(os.Stderr, "mlr script: %v\n", err)
os.Exit(1) return 1
} }
err = scr.closeBufferedOutputStream() err = scr.closeBufferedOutputStream()
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "mlr script: %v\n", err) fmt.Fprintf(os.Stderr, "mlr script: %v\n", err)
os.Exit(1) return 1
} }
return 0 return 0
} }

View file

@ -57,19 +57,24 @@ func Dispatchable(arg string) bool {
return false 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 { 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] terminal := args[0]
for _, entry := range _TERMINAL_LOOKUP_TABLE { for _, entry := range _TERMINAL_LOOKUP_TABLE {
if terminal == entry.name { if terminal == entry.name {
os.Exit(entry.main(args)) return entry.main(args)
} }
} }
fmt.Fprintf(os.Stderr, "mlr: terminal \"%s\" not found.\n", terminal) fmt.Fprintf(os.Stderr, "mlr: terminal \"%s\" not found.\n", terminal)
os.Exit(1) return 1
} }
// terminalListMain is the handler for 'mlr terminal-list'. // terminalListMain is the handler for 'mlr terminal-list'.

View file

@ -65,7 +65,10 @@ func TestRegression(t *testing.T) {
firstNFailsToShow, firstNFailsToShow,
) )
ok := regtester.Execute(casePaths) ok, err := regtester.Execute(casePaths)
if err != nil {
t.Fatal(err)
}
if !ok { if !ok {
t.Fatal() t.Fatal()
} }