mirror of
https://github.com/johnkerl/miller.git
synced 2026-07-17 16:38:54 +00:00
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:
parent
707e3dbf06
commit
40ac1b309e
21 changed files with 171 additions and 171 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
24
pkg/lib/exit.go
Normal 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}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue