Remove os.Exit callsites below the entrypoint: phase 1 (#2198)

* Rebuild docs: help-catalog index count drifted on main (666 -> 667)

Pre-existing drift from a recent catalog addition; surfaced by make dev.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Remove os.Exit callsites below the entrypoint: phase 1 (plans/exit.md)

Phase 1 of plans/exit.md: the mechanical swaps, plus the single-exit-point
scaffolding.

- New sentinel lib.ExitRequest{Code} (io.EOF-style control flow): returned by
  code paths that have already printed what they wanted (--version, -h,
  'put --explain' on a valid expression, 'put -x') instead of exiting mid-stack.
- pkg/entrypoint: new exitOnError() maps errors to exit codes in one place:
  ErrHelpRequested -> 0, ErrUsagePrinted -> 1, ExitRequest -> its code,
  anything else printed (JSON if --errors-json) -> 1.
- pkg/climain: version/help/usage/validation exits become returned errors;
  the ErrHelpRequested/ErrUsagePrinted -> exit translation moves up to the
  entrypoint; loadMlrrcOrDie becomes error-returning loadMlrrcFiles.
- Transformer ParseCLI/constructor exits -> returned errors (cut,
  having_fields, nest, reorder, merge_fields, reshape, rename, split, tee,
  subs, put_or_filter). merge_fields' bad-regex message formerly mis-reported
  itself as coming from the cut verb; tee's unrecognized-option exit was
  formerly silent; split/tee constructor failures formerly exited without
  printing the constructor's error.
- YAML/JSON record-writer marshal errors -> returned through Write, matching
  the CSV writer's error path.
- lib.WriteTempFileOrDie -> WriteTempFile (string, error); its sole caller
  (regtest diff helper) degrades gracefully.

Stderr messages and exit codes are byte-identical for all regression-covered
paths (4779 cases pass); runtime Transform-path exits are phase 3.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Add lib.NewExitZeroRequest() constructor for exit-0 sentinel returns

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
John Kerl 2026-07-15 11:34:01 -04:00 committed by GitHub
parent 707e3dbf06
commit 40ac1b309e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 171 additions and 171 deletions

View file

@ -64,7 +64,7 @@ one-line summary (here trimmed, and then counted, using Miller itself):
<pre class="pre-non-highlight-in-pair">
[
{
"count": 666
"count": 667
}
]
</pre>

View file

@ -11,19 +11,7 @@ import (
"github.com/johnkerl/miller/v6/pkg/cli"
)
// loadMlrrcOrDie is a fatal-error wrapper around loadMlrrc.
func loadMlrrcOrDie(
options *cli.TOptions,
profileName string,
) {
err := loadMlrrc(options, profileName)
if err != nil {
fmt.Fprintf(os.Stderr, "mlr: %v\n", err)
os.Exit(1)
}
}
// loadMlrrc rule: If $MLRRC is set, use it and only it. Otherwise try first
// loadMlrrcFiles rule: If $MLRRC is set, use it and only it. Otherwise try first
// $HOME/.mlrrc and then ./.mlrrc but let them stack: e.g. $HOME/.mlrrc is
// lots of settings and maybe in one subdir you want to override just a
// setting or two.
@ -34,7 +22,7 @@ func loadMlrrcOrDie(
// skipped. Non-empty means global lines are applied first, then the lines in
// any [profileName] section. It's a fatal error if a profile was requested
// but no matching section exists in any .mlrrc file processed.
func loadMlrrc(
func loadMlrrcFiles(
options *cli.TOptions,
profileName string,
) error {
@ -85,7 +73,7 @@ func loadMlrrc(
return checkMlrrcProfileWasFound(profileName, foundProfile, loadedPaths)
}
// checkMlrrcProfileWasFound is a helper function for loadMlrrc: if a profile
// checkMlrrcProfileWasFound is a helper function for loadMlrrcFiles: if a profile
// was requested via --profile {name} / -P {name}, there must be a matching
// [name] section in at least one processed .mlrrc file.
func checkMlrrcProfileWasFound(
@ -108,7 +96,7 @@ func checkMlrrcProfileWasFound(
)
}
// tryLoadMlrrc is a helper function for loadMlrrc. The first return value is
// tryLoadMlrrc is a helper function for loadMlrrcFiles. The first return value is
// whether the file could be opened at all: an unopenable file is not an error
// (that's the normal case when no .mlrrc file exists). The second is any
// parse error within an opened file.

View file

@ -39,7 +39,7 @@ func TestMlrrcGlobalOnlyBackCompat(t *testing.T) {
// .mlrrc format.
writeTempMlrrc(t, "icsv\nojson\n")
options := cli.DefaultOptions()
if err := loadMlrrc(options, ""); err != nil {
if err := loadMlrrcFiles(options, ""); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if options.ReaderOptions.InputFileFormat != "csv" {
@ -53,7 +53,7 @@ func TestMlrrcGlobalOnlyBackCompat(t *testing.T) {
func TestMlrrcSectionsIgnoredWithoutProfile(t *testing.T) {
writeTempMlrrc(t, testMlrrcContents)
options := cli.DefaultOptions()
if err := loadMlrrc(options, ""); err != nil {
if err := loadMlrrcFiles(options, ""); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if options.ReaderOptions.InputFileFormat != "csv" {
@ -68,7 +68,7 @@ func TestMlrrcSectionsIgnoredWithoutProfile(t *testing.T) {
func TestMlrrcProfileSelection(t *testing.T) {
writeTempMlrrc(t, testMlrrcContents)
options := cli.DefaultOptions()
if err := loadMlrrc(options, "j"); err != nil {
if err := loadMlrrcFiles(options, "j"); err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Global setting applies first ...
@ -84,7 +84,7 @@ func TestMlrrcProfileSelection(t *testing.T) {
func TestMlrrcRepeatedSectionsAccumulate(t *testing.T) {
writeTempMlrrc(t, testMlrrcContents)
options := cli.DefaultOptions()
if err := loadMlrrc(options, "j"); err != nil {
if err := loadMlrrcFiles(options, "j"); err != nil {
t.Fatalf("unexpected error: %v", err)
}
// no-auto-flatten is in the second [j] block.
@ -96,7 +96,7 @@ func TestMlrrcRepeatedSectionsAccumulate(t *testing.T) {
func TestMlrrcOtherProfileNotApplied(t *testing.T) {
writeTempMlrrc(t, testMlrrcContents)
options := cli.DefaultOptions()
if err := loadMlrrc(options, "p"); err != nil {
if err := loadMlrrcFiles(options, "p"); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if options.WriterOptions.OutputFileFormat != "pprint" {
@ -107,7 +107,7 @@ func TestMlrrcOtherProfileNotApplied(t *testing.T) {
func TestMlrrcMissingProfileIsError(t *testing.T) {
writeTempMlrrc(t, testMlrrcContents)
options := cli.DefaultOptions()
err := loadMlrrc(options, "nonesuch")
err := loadMlrrcFiles(options, "nonesuch")
if err == nil {
t.Fatal("expected error for missing profile, got nil")
}
@ -119,7 +119,7 @@ func TestMlrrcMissingProfileIsError(t *testing.T) {
func TestMlrrcProfileWithMlrrcNoneIsError(t *testing.T) {
t.Setenv("MLRRC", "__none__")
options := cli.DefaultOptions()
err := loadMlrrc(options, "j")
err := loadMlrrcFiles(options, "j")
if err == nil {
t.Fatal("expected error for profile with MLRRC=__none__, got nil")
}
@ -132,7 +132,7 @@ func TestMlrrcProfileWithNoMlrrcFileIsError(t *testing.T) {
t.Setenv("MLRRC", filepath.Join(t.TempDir(), "nonexistent"))
t.Setenv("HOME", t.TempDir())
options := cli.DefaultOptions()
err := loadMlrrc(options, "j")
err := loadMlrrcFiles(options, "j")
if err == nil {
t.Fatal("expected error for profile with no .mlrrc file, got nil")
}
@ -144,7 +144,7 @@ func TestMlrrcProfileWithNoMlrrcFileIsError(t *testing.T) {
func TestMlrrcWhitespaceAndCommentsAroundSectionHeaders(t *testing.T) {
writeTempMlrrc(t, "icsv\n\n [ j ] # a comment\nojson\n")
options := cli.DefaultOptions()
if err := loadMlrrc(options, "j"); err != nil {
if err := loadMlrrcFiles(options, "j"); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if options.WriterOptions.OutputFileFormat != "json" {
@ -156,7 +156,7 @@ func TestMlrrcMalformedSectionHeadersAreErrors(t *testing.T) {
for _, header := range []string{"[j", "[]", "[ ]", "[a[b]"} {
writeTempMlrrc(t, header+"\n")
options := cli.DefaultOptions()
err := loadMlrrc(options, "")
err := loadMlrrcFiles(options, "")
if err == nil {
t.Errorf("expected parse error for header %q, got nil", header)
} else if !strings.Contains(err.Error(), "parse error") {
@ -170,12 +170,12 @@ func TestMlrrcUnusedProfileLinesNotValidated(t *testing.T) {
// invocations of mlr.
writeTempMlrrc(t, "icsv\n[j]\nthis-is-not-a-flag\n")
options := cli.DefaultOptions()
if err := loadMlrrc(options, ""); err != nil {
if err := loadMlrrcFiles(options, ""); err != nil {
t.Fatalf("unexpected error: %v", err)
}
// But it is a parse error when the section is selected.
options = cli.DefaultOptions()
if err := loadMlrrc(options, "j"); err == nil {
if err := loadMlrrcFiles(options, "j"); err == nil {
t.Fatal("expected parse error for selected profile with bad line, got nil")
}
}
@ -183,7 +183,7 @@ func TestMlrrcUnusedProfileLinesNotValidated(t *testing.T) {
func TestMlrrcGlobalParseErrorsStillFatal(t *testing.T) {
writeTempMlrrc(t, "this-is-not-a-flag\n")
options := cli.DefaultOptions()
if err := loadMlrrc(options, ""); err == nil {
if err := loadMlrrcFiles(options, ""); err == nil {
t.Fatal("expected parse error, got nil")
}
}
@ -192,7 +192,7 @@ func TestMlrrcPrepipeStillDisallowed(t *testing.T) {
// Code-execution flags are disallowed in .mlrrc -- inside profiles too.
writeTempMlrrc(t, "[j]\nprepipe zcat\n")
options := cli.DefaultOptions()
if err := loadMlrrc(options, "j"); err == nil {
if err := loadMlrrcFiles(options, "j"); err == nil {
t.Fatal("expected error for --prepipe within a profile, got nil")
}
}
@ -203,7 +203,7 @@ func TestMlrrcProfileFlagDisallowedWithinMlrrc(t *testing.T) {
for _, line := range []string{"profile j", "--profile j", "-P j"} {
writeTempMlrrc(t, line+"\n")
options := cli.DefaultOptions()
err := loadMlrrc(options, "")
err := loadMlrrcFiles(options, "")
if err == nil {
t.Errorf("expected parse error for %q within .mlrrc, got nil", line)
} else if !strings.Contains(err.Error(), "parse error") {
@ -213,7 +213,7 @@ func TestMlrrcProfileFlagDisallowedWithinMlrrc(t *testing.T) {
// Inside a selected profile section, too.
writeTempMlrrc(t, "[j]\nprofile p\n")
options := cli.DefaultOptions()
if err := loadMlrrc(options, "j"); err == nil {
if err := loadMlrrcFiles(options, "j"); err == nil {
t.Fatal("expected parse error for --profile within a profile section, got nil")
}
}

View file

@ -151,18 +151,23 @@ func parseCommandLinePassOne(
if args[argi][0] == '-' {
if args[argi] == registry.VersionFlag {
// Exiting flag: handle it immediately.
// Exiting flag: print immediately, then request exit 0 from the
// entrypoint.
fmt.Printf("mlr %s\n", version.STRING)
os.Exit(0)
parseErr = lib.NewExitZeroRequest()
return
} else if args[argi] == registry.BareVersionFlag {
// Exiting flag: handle it immediately.
// Exiting flag: print immediately, then request exit 0 from the
// entrypoint.
fmt.Printf("%s\n", version.STRING)
os.Exit(0)
parseErr = lib.NewExitZeroRequest()
return
} else if help.ParseTerminalUsage(args[argi]) {
// Exiting flag: handle it immediately.
// Exiting flag: the usage text has been printed.
// Most help is in the 'mlr help' terminal but there are a few
// shorthands like 'mlr -h' and 'mlr -F'.
os.Exit(0)
parseErr = lib.NewExitZeroRequest()
return
} else if args[argi] == "--norc" {
argi += 1
@ -196,13 +201,15 @@ func parseCommandLinePassOne(
// The first verb in the then-chain can *optionally* be preceded by
// 'then'. The others one *must* be.
if args[argi] == "then" || args[argi] == "+" {
cli.CheckArgCount(args, argi, argc, 1)
oargi++
argi++
}
if argi >= argc {
fmt.Fprintln(os.Stderr, "mlr: 'then' must have a verb after it.")
os.Exit(1)
parseErr = &CLIError{
Kind: "generic",
Msg: "mlr: 'then' must have a verb after it.",
}
return
}
verb := args[argi]
onFirst = false
@ -226,11 +233,11 @@ func parseCommandLinePassOne(
false, // false for first pass of CLI-parse, true for second pass -- this is the first pass
)
if err != nil {
if errors.Is(err, cli.ErrHelpRequested) {
os.Exit(0)
}
if errors.Is(err, cli.ErrUsagePrinted) {
os.Exit(1)
if errors.Is(err, cli.ErrHelpRequested) || errors.Is(err, cli.ErrUsagePrinted) {
// Sentinel errors: any output has already been printed; the
// entrypoint maps these to their exit codes.
parseErr = err
return
}
// Return a structured error so the caller can emit JSON or
// prose depending on --errors-json.
@ -268,7 +275,8 @@ func parseCommandLinePassOne(
// piped stdin) defaults to 'cat'.
if argc == 1 {
help.MainUsage(os.Stderr)
os.Exit(1)
parseErr = cli.ErrUsagePrinted
return
}
verbSequences = append(verbSequences, []string{"cat"})
}
@ -326,13 +334,17 @@ func parseCommandLinePassTwo(
}
}
if loadMlrrc {
loadMlrrcOrDie(options, mlrrcProfileName)
if err = loadMlrrcFiles(options, mlrrcProfileName); err != nil {
return nil, nil, err
}
} else if mlrrcProfileName != "" {
fmt.Fprintf(os.Stderr,
"mlr: --profile \"%s\" was specified along with --norc, which disables .mlrrc processing.\n",
mlrrcProfileName,
)
os.Exit(1)
return nil, nil, &CLIError{
Kind: "generic",
Msg: fmt.Sprintf(
"mlr: --profile \"%s\" was specified along with --norc, which disables .mlrrc processing.",
mlrrcProfileName,
),
}
}
// Process the flag-sequences in order from pass one. We assume all the
@ -402,12 +414,8 @@ func parseCommandLinePassTwo(
true, // false for first pass of CLI-parse, true for second pass -- this is pass two
)
if err != nil {
if errors.Is(err, cli.ErrHelpRequested) {
os.Exit(0)
}
if errors.Is(err, cli.ErrUsagePrinted) {
os.Exit(1)
}
// Sentinel errors (ErrHelpRequested, ErrUsagePrinted) pass through
// here too; the entrypoint maps them to their exit codes.
return nil, nil, err
}
// Unparsable verb-setups should have been found in pass one.
@ -455,8 +463,10 @@ func parseCommandLinePassTwo(
}
if options.DoInPlace && len(options.FileNames) == 0 {
fmt.Fprintf(os.Stderr, "%s: -I option (in-place operation) requires input files.\n", "mlr")
os.Exit(1)
return nil, nil, &CLIError{
Kind: "generic",
Msg: "mlr: -I option (in-place operation) requires input files.",
}
}
if options.HaveRandSeed {

View file

@ -4,6 +4,7 @@
package entrypoint
import (
"errors"
"fmt"
"os"
"path"
@ -48,12 +49,7 @@ func Main() MainReturn {
options, recordTransformers, err := climain.ParseCommandLine(os.Args)
if err != nil {
if wantJSON {
climain.EmitStructuredError(err)
} else {
printError(err)
}
os.Exit(1)
exitOnError(err, wantJSON)
}
if !options.DoInPlace {
@ -62,8 +58,7 @@ func Main() MainReturn {
err = processFilesInPlace(options)
}
if err != nil {
printError(err)
os.Exit(1)
exitOnError(err, false)
}
return MainReturn{
@ -71,6 +66,29 @@ func Main() MainReturn {
}
}
// exitOnError terminates the process for a non-nil error from command-line
// parsing or stream processing. Sentinel errors carry their own exit codes
// and have already produced whatever output they wanted; any other error is
// printed (structured if --errors-json is active) and exits 1. This is
// intended to be the single os.Exit point below main; see plans/exit.md.
func exitOnError(err error, wantJSON bool) {
var exitRequest *lib.ExitRequest
switch {
case errors.Is(err, cli.ErrHelpRequested):
os.Exit(0)
case errors.Is(err, cli.ErrUsagePrinted):
os.Exit(1)
case errors.As(err, &exitRequest):
os.Exit(exitRequest.Code)
case wantJSON:
climain.EmitStructuredError(err)
os.Exit(1)
default:
printError(err)
os.Exit(1)
}
}
// printError prints err to stderr. Errors that already carry an "mlr" prefix
// (e.g. "mlr stats1: ..." or "mlr: verb not found") are printed as-is to
// avoid double-prefixing.

24
pkg/lib/exit.go Normal file
View file

@ -0,0 +1,24 @@
package lib
import "fmt"
// ExitRequest is a sentinel error requesting process termination with the
// given exit code. Code paths that have already produced all the output they
// want (e.g. --version, or 'mlr put --explain' on a valid expression) return
// an ExitRequest instead of calling os.Exit mid-stack; the entrypoint unwraps
// it with errors.As and exits with the requested code. This is control flow
// in the io.EOF style: an "error" that isn't a failure. See plans/exit.md.
type ExitRequest struct {
Code int
}
func (e *ExitRequest) Error() string {
return fmt.Sprintf("exit status %d", e.Code)
}
// NewExitZeroRequest is for the common case of requesting a normal, exit-0
// termination: e.g. --version or -h, where the requested output has been
// printed and nothing further should run.
func NewExitZeroRequest() *ExitRequest {
return &ExitRequest{Code: 0}
}

View file

@ -158,27 +158,24 @@ func GetArrayKeysSorted(input map[string]string) []string {
// WriteTempFile places the contents string into a temp file, which the caller
// must remove.
func WriteTempFileOrDie(contents string) string {
func WriteTempFile(contents string) (string, error) {
// Use "" as first argument to os.CreateTemp to use default directory.
// Nominally "/tmp" or somesuch on all unix-like systems, but not for Windows.
handle, err := os.CreateTemp("", "mlr-temp")
if err != nil {
fmt.Printf("mlr: could not create temp file.\n")
os.Exit(1)
return "", fmt.Errorf("could not create temp file: %w", err)
}
_, err = handle.WriteString(contents)
if err != nil {
fmt.Printf("mlr: could not populate temp file.\n")
os.Exit(1)
return "", fmt.Errorf("could not populate temp file: %w", err)
}
err = handle.Close()
if err != nil {
fmt.Printf("mlr: could not finish write of temp file.\n")
os.Exit(1)
return "", fmt.Errorf("could not finish write of temp file: %w", err)
}
return handle.Name()
return handle.Name(), nil
}
func StripEmpties(input []string) []string {

View file

@ -2,8 +2,6 @@ package output
import (
"bufio"
"fmt"
"os"
"github.com/johnkerl/miller/v6/pkg/cli"
"github.com/johnkerl/miller/v6/pkg/mlrval"
@ -56,11 +54,9 @@ func (writer *RecordWriterJSON) Write(
}
if writer.writerOptions.WrapJSONOutputInOuterList {
writer.writeWithListWrap(outrec, context, bufferedOutputStream, outputIsStdout)
} else {
writer.writeWithoutListWrap(outrec, context, bufferedOutputStream, outputIsStdout)
return writer.writeWithListWrap(outrec, context, bufferedOutputStream, outputIsStdout)
}
return nil
return writer.writeWithoutListWrap(outrec, context, bufferedOutputStream, outputIsStdout)
}
func (writer *RecordWriterJSON) writeWithListWrap(
@ -68,7 +64,7 @@ func (writer *RecordWriterJSON) writeWithListWrap(
context *types.Context,
bufferedOutputStream *bufio.Writer,
outputIsStdout bool,
) {
) error {
if outrec != nil { // Not end of record stream
if !writer.wroteAnyRecords {
bufferedOutputStream.WriteString("[\n")
@ -78,8 +74,7 @@ func (writer *RecordWriterJSON) writeWithListWrap(
// can place it neatly with commas here (if the user requested them).
s, err := outrec.FormatAsJSON(writer.jsonFormatting, outputIsStdout)
if err != nil {
fmt.Fprintf(os.Stderr, "mlr: %v\n", err)
os.Exit(1)
return err
}
if writer.wroteAnyRecords {
@ -102,6 +97,7 @@ func (writer *RecordWriterJSON) writeWithListWrap(
}
}
return nil
}
func (writer *RecordWriterJSON) writeWithoutListWrap(
@ -109,20 +105,20 @@ func (writer *RecordWriterJSON) writeWithoutListWrap(
_ *types.Context,
bufferedOutputStream *bufio.Writer,
outputIsStdout bool,
) {
) error {
if outrec == nil {
// End of record stream
return
return nil
}
// The Mlrmap FormatAsJSON doesn't include the final newline, so that we
// can place it neatly with commas here (if the user requested them).
s, err := outrec.FormatAsJSON(writer.jsonFormatting, outputIsStdout)
if err != nil {
fmt.Fprintf(os.Stderr, "mlr: %v\n", err)
os.Exit(1)
return err
}
bufferedOutputStream.WriteString(s)
bufferedOutputStream.WriteString("\n")
return nil
}

View file

@ -2,8 +2,6 @@ package output
import (
"bufio"
"fmt"
"os"
"gopkg.in/yaml.v3"
@ -33,25 +31,22 @@ func (writer *RecordWriterYAML) Write(
outputIsStdout bool,
) error {
if writer.writerOptions.WrapYAMLOutputInOuterList {
writer.writeWithListWrap(outrec, bufferedOutputStream)
} else {
writer.writeWithoutListWrap(outrec, bufferedOutputStream)
return writer.writeWithListWrap(outrec, bufferedOutputStream)
}
return nil
return writer.writeWithoutListWrap(outrec, bufferedOutputStream)
}
func (writer *RecordWriterYAML) writeWithListWrap(
outrec *mlrval.Mlrmap,
bufferedOutputStream *bufio.Writer,
) {
) error {
if outrec != nil {
if writer.bufferedRecords == nil {
writer.bufferedRecords = []*yaml.Node{}
}
native, err := mlrval.MlrmapToYAMLNative(outrec)
if err != nil {
fmt.Fprintf(os.Stderr, "mlr: %v\n", err)
os.Exit(1)
return err
}
writer.bufferedRecords = append(writer.bufferedRecords, native)
} else {
@ -60,34 +55,33 @@ func (writer *RecordWriterYAML) writeWithListWrap(
seqNode.Content = writer.bufferedRecords
out, err := yaml.Marshal(seqNode)
if err != nil {
fmt.Fprintf(os.Stderr, "mlr: %v\n", err)
os.Exit(1)
return err
}
bufferedOutputStream.Write(out)
writer.bufferedRecords = nil
}
return nil
}
func (writer *RecordWriterYAML) writeWithoutListWrap(
outrec *mlrval.Mlrmap,
bufferedOutputStream *bufio.Writer,
) {
) error {
if outrec == nil {
return
return nil
}
if writer.wroteAnyRecords {
bufferedOutputStream.WriteString("---\n")
}
native, err := mlrval.MlrmapToYAMLNative(outrec)
if err != nil {
fmt.Fprintf(os.Stderr, "mlr: %v\n", err)
os.Exit(1)
return err
}
out, err := yaml.Marshal(native)
if err != nil {
fmt.Fprintf(os.Stderr, "mlr: %v\n", err)
os.Exit(1)
return err
}
bufferedOutputStream.Write(out)
writer.wroteAnyRecords = true
return nil
}

View file

@ -2,6 +2,7 @@ package regtest
import (
"bytes"
"fmt"
"os"
"os/exec"
"strings"
@ -70,9 +71,17 @@ func RunDiffCommandOnStrings(
) (
diffOutput string,
) {
actualOutputFileName := lib.WriteTempFileOrDie(actualOutput)
expectedOutputFileName := lib.WriteTempFileOrDie(expectedOutput)
// The diff output is best-effort debugging help (see below); if we can't
// write the temp files for it, say so in its place rather than aborting.
actualOutputFileName, err := lib.WriteTempFile(actualOutput)
if err != nil {
return fmt.Sprintf("(diff unavailable: %v)", err)
}
defer func() { _ = os.Remove(actualOutputFileName) }()
expectedOutputFileName, err := lib.WriteTempFile(expectedOutput)
if err != nil {
return fmt.Sprintf("(diff unavailable: %v)", err)
}
defer func() { _ = os.Remove(expectedOutputFileName) }()
// This is diff or fc

View file

@ -162,12 +162,7 @@ func NewTransformerCut(
// Handles "a.*b"i Miller case-insensitive-regex specification
regex, err := lib.CompileMillerRegex(regexString)
if err != nil {
fmt.Fprintf(
os.Stderr,
"%s %s: cannot compile regex [%s]\n",
"mlr", verbNameCut, regexString,
)
os.Exit(1)
return nil, cli.VerbErrorf(verbNameCut, "cannot compile regex [%s]", regexString)
}
tr.regexes[i] = regex
}

View file

@ -207,15 +207,7 @@ func NewTransformerHavingFields(
// Strip off the leading " and trailing " or "i.
regex, err := lib.CompileMillerRegex(regexString)
if err != nil {
fmt.Fprintf(
os.Stderr,
"%s %s: cannot compile regex \"%s\"\n",
"mlr",
verbNameHavingFields,
regexString,
)
os.Exit(1)
// return nil, err
return nil, cli.VerbErrorf(verbNameHavingFields, "cannot compile regex \"%s\"", regexString)
}
tr.regex = regex

View file

@ -282,12 +282,8 @@ func NewTransformerMergeFields(
// Handles "a.*b"i Miller case-insensitive-regex specification
regex, err := lib.CompileMillerRegex(regexString)
if err != nil {
fmt.Fprintf(
os.Stderr,
"%s %s: cannot compile regex [%s]\n",
"mlr", verbNameCut, regexString,
)
os.Exit(1)
// (This formerly mis-reported itself as coming from the cut verb.)
return nil, cli.VerbErrorf(verbNameMergeFields, "cannot compile regex [%s]", regexString)
}
tr.valueFieldNameRegexes[i] = regex
}

View file

@ -313,12 +313,7 @@ func NewTransformerNest(
if doRegexes {
fieldRegex, err := lib.CompileMillerRegex(fieldName)
if err != nil {
fmt.Fprintf(
os.Stderr,
"%s %s: cannot compile regex [%s]\n",
"mlr", verbNameNest, fieldName,
)
os.Exit(1)
return nil, cli.VerbErrorf(verbNameNest, "cannot compile regex [%s]", fieldName)
}
tr.fieldRegex = fieldRegex
// implode uses fieldRegex directly when doRegexes
@ -327,12 +322,7 @@ func NewTransformerNest(
regexString := "^" + fieldName + "_[0-9]+$"
regex, err := lib.CompileMillerRegex(regexString)
if err != nil {
fmt.Fprintf(
os.Stderr,
"%s %s: cannot compile regex [%s]\n",
"mlr", verbNameNest, regexString,
)
os.Exit(1)
return nil, cli.VerbErrorf(verbNameNest, "cannot compile regex [%s]", regexString)
}
tr.regex = regex
}

View file

@ -459,7 +459,8 @@ func NewTransformerPut(
// --explain is a validate/dry-run: report whether the DSL parsed and
// type-checked, then exit without reading the input stream. A parse/build
// error is returned so it flows through the normal error path (including
// --errors-json); a valid expression prints a confirmation and exits 0.
// --errors-json); a valid expression prints a confirmation and requests
// exit 0 via the ExitRequest sentinel.
if doExplain {
if err != nil {
return nil, err
@ -469,11 +470,10 @@ func NewTransformerPut(
verbName = "filter"
}
if warningsAreFatal && hadWarnings {
fmt.Fprintf(os.Stderr, "mlr %s: DSL expression has warnings treated as fatal.\n", verbName)
os.Exit(1)
return nil, cli.VerbErrorf(verbName, "DSL expression has warnings treated as fatal.")
}
fmt.Printf("mlr %s: DSL expression is valid.\n", verbName)
os.Exit(0)
return nil, lib.NewExitZeroRequest()
}
if warningsAreFatal && hadWarnings {
@ -481,11 +481,11 @@ func NewTransformerPut(
"%s: Exiting due to warnings treated as fatal.\n",
"mlr",
)
os.Exit(1)
return nil, &lib.ExitRequest{Code: 1}
}
if exitAfterParse {
os.Exit(0)
return nil, lib.NewExitZeroRequest()
}
if err != nil {

View file

@ -164,7 +164,10 @@ func NewTransformerRename(
tr.regexesAndReplacements = []*tRegexAndReplacement{}
for pe := oldToNewNames.Head; pe != nil; pe = pe.Next {
regexString := pe.Key
regex := lib.CompileMillerRegexOrDie(regexString)
regex, err := lib.CompileMillerRegex(regexString)
if err != nil {
return nil, err
}
replacement := pe.Value
_, replacementCaptureMatrix := lib.ReplacementHasCaptures(replacement)
regexAndReplacement := tRegexAndReplacement{

View file

@ -196,12 +196,7 @@ func NewTransformerReorder(
// Handles "a.*b"i Miller case-insensitive-regex specification
regex, err := lib.CompileMillerRegex(regexString)
if err != nil {
fmt.Fprintf(
os.Stderr,
"%s %s: cannot compile regex [%s]\n",
"mlr", verbNameReorder, regexString,
)
os.Exit(1)
return nil, cli.VerbErrorf(verbNameReorder, "cannot compile regex [%s]", regexString)
}
tr.regexes[i] = regex
}

View file

@ -276,12 +276,7 @@ func NewTransformerReshape(
for i, inputFieldRegexString := range inputFieldRegexStrings {
regex, err := lib.CompileMillerRegex(inputFieldRegexString)
if err != nil {
fmt.Fprintf(
os.Stderr,
"%s %s: cannot compile regex [%s]\n",
"mlr", verbNameReshape, inputFieldRegexString,
)
os.Exit(1)
return nil, cli.VerbErrorf(verbNameReshape, "cannot compile regex [%s]", inputFieldRegexString)
}
tr.inputFieldRegexes[i] = regex
}

View file

@ -230,8 +230,7 @@ func transformerSplitParseCLI(
&localOptions.WriterOptions,
)
if err != nil {
// Error message already printed out
os.Exit(1)
return nil, err
}
return transformer, nil

View file

@ -177,7 +177,7 @@ func transformerSubsParseCLI(
switch opt {
case "-h", "--help":
usageFunc(os.Stdout)
os.Exit(0)
return nil, cli.ErrHelpRequested
case "-a":
doAllFieldNames = true
@ -195,19 +195,19 @@ func transformerSubsParseCLI(
doAllFieldNames = false
default:
usageFunc(os.Stderr)
os.Exit(1)
return nil, cli.ErrUsagePrinted
}
}
if fieldNames == nil && !doAllFieldNames {
usageFunc(os.Stderr)
os.Exit(1)
return nil, cli.ErrUsagePrinted
}
// Get the old and new text from the command line
if (argc - argi) < 2 {
usageFunc(os.Stderr)
os.Exit(1)
return nil, cli.ErrUsagePrinted
}
oldText = args[argi]
newText = args[argi+1]

View file

@ -95,7 +95,7 @@ func transformerTeeParseCLI(
// Nothing else to handle here.
argi = largi
} else {
os.Exit(1)
return nil, cli.VerbErrorf(verbNameTee, "option \"%s\" not recognized", opt)
}
}
}
@ -123,8 +123,7 @@ func transformerTeeParseCLI(
&localOptions.WriterOptions,
)
if err != nil {
// Error message already printed out
os.Exit(1)
return nil, err
}
return transformer, nil