mirror of
https://github.com/johnkerl/miller.git
synced 2026-07-18 00:45:47 +00:00
* 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>
238 lines
6.8 KiB
Go
238 lines
6.8 KiB
Go
// All the usual contents of main() are put into this package for ease of
|
|
// testing.
|
|
|
|
package entrypoint
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
|
|
"github.com/johnkerl/miller/v6/pkg/auxents"
|
|
"github.com/johnkerl/miller/v6/pkg/cli"
|
|
"github.com/johnkerl/miller/v6/pkg/climain"
|
|
"github.com/johnkerl/miller/v6/pkg/lib"
|
|
"github.com/johnkerl/miller/v6/pkg/platform"
|
|
"github.com/johnkerl/miller/v6/pkg/stream"
|
|
"github.com/johnkerl/miller/v6/pkg/transformers"
|
|
)
|
|
|
|
type MainReturn struct {
|
|
PrintElapsedTime bool
|
|
}
|
|
|
|
func Main() MainReturn {
|
|
// Special handling for Windows so we can do things like:
|
|
//
|
|
// mlr put '$a = $b . "cd \"efg\" hi"' foo.dat
|
|
//
|
|
// as on Linux/Unix/MacOS. (On the latter platforms, this is just os.Args
|
|
// as-is.)
|
|
os.Args = platform.GetArgs()
|
|
|
|
// Enable ANSI escape sequence processing on the Windows pseudo terminal,
|
|
// otherwise, we only raw ANSI escape sequences like ←[0;30m 0←[0m ←[0;31m 1
|
|
platform.EnableAnsiEscapeSequences()
|
|
|
|
// 'mlr repl' or 'mlr lecat' or any other non-miller-per-se toolery which
|
|
// is delivered (for convenience) within the mlr executable. If argv[1] is
|
|
// found then this function will not return.
|
|
auxents.Dispatch(os.Args)
|
|
|
|
if lib.IsTruthyEnvValue(os.Getenv("MLR_NO_SHELL")) {
|
|
lib.DisableShellOut()
|
|
}
|
|
|
|
wantJSON := climain.WantErrorsJSON(os.Args)
|
|
|
|
options, recordTransformers, err := climain.ParseCommandLine(os.Args)
|
|
if err != nil {
|
|
exitOnError(err, wantJSON)
|
|
}
|
|
|
|
if !options.DoInPlace {
|
|
err = processToStdout(options, recordTransformers)
|
|
} else {
|
|
err = processFilesInPlace(options)
|
|
}
|
|
if err != nil {
|
|
exitOnError(err, false)
|
|
}
|
|
|
|
return MainReturn{
|
|
PrintElapsedTime: options.PrintElapsedTime,
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
func printError(err error) {
|
|
msg := err.Error()
|
|
if strings.HasPrefix(msg, "mlr") {
|
|
fmt.Fprintf(os.Stderr, "%v\n", msg)
|
|
} else {
|
|
fmt.Fprintf(os.Stderr, "mlr: %v\n", msg)
|
|
}
|
|
}
|
|
|
|
// processToStdout is normal processing without mlr -I.
|
|
|
|
func processToStdout(
|
|
options *cli.TOptions,
|
|
recordTransformers []transformers.RecordTransformer,
|
|
) error {
|
|
return stream.Stream(options.FileNames, options, recordTransformers, os.Stdout, true)
|
|
}
|
|
|
|
// processFilesInPlace is in-place processing without mlr -I.
|
|
//
|
|
// For in-place mode, reconstruct the transformers on each input file. E.g.
|
|
// 'mlr -I head -n 2 foo bar' should do head -n 2 on foo as well as on bar.
|
|
//
|
|
// I could have implemented this with a single construction of the transformers
|
|
// and having each transformers implement a Reset() method. However, having
|
|
// effectively two initializers per transformers -- constructor and reset method
|
|
// -- I'd surely miss some logic somewhere. With in-place mode being a less
|
|
// frequently used code path, this would likely lead to latent bugs. So this
|
|
// approach leads to greater code stability.
|
|
|
|
func processFilesInPlace(
|
|
originalOptions *cli.TOptions,
|
|
) error {
|
|
// This should have been already checked by the CLI parser when validating
|
|
// the -I flag.
|
|
lib.InternalCodingErrorIf(originalOptions.FileNames == nil)
|
|
lib.InternalCodingErrorIf(len(originalOptions.FileNames) == 0)
|
|
|
|
// Save off the file names from the command line.
|
|
fileNames := make([]string, len(originalOptions.FileNames))
|
|
copy(fileNames, originalOptions.FileNames)
|
|
|
|
for _, fileName := range fileNames {
|
|
err := processFileInPlace(fileName, originalOptions)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func processFileInPlace(
|
|
fileName string,
|
|
originalOptions *cli.TOptions,
|
|
) error {
|
|
|
|
if _, err := os.Stat(fileName); os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
|
|
// Reconstruct the transformers for each file name, and allocate
|
|
// reader, mappers, and writer individually for each file name. This
|
|
// way CSV headers appear in each file, head -n 10 puts 10 rows for
|
|
// each output file, and so on.
|
|
options, recordTransformers, err := climain.ParseCommandLine(os.Args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// We can't in-place update http://, https://, etc. Also, anything with
|
|
// --prepipe or --prepipex, we won't try to guess how to invert that
|
|
// command to produce re-compressed output.
|
|
err = lib.IsUpdateableInPlace(fileName, options.ReaderOptions.Prepipe)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get the original file's mode so we can preserve it.
|
|
fileInfo, err := os.Stat(fileName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
originalMode := fileInfo.Mode()
|
|
|
|
containingDirectory := path.Dir(fileName)
|
|
// Names like ./mlr-in-place-2148227797 and ./mlr-in-place-1792078347,
|
|
// as revealed by printing handle.Name().
|
|
handle, err := os.CreateTemp(containingDirectory, "mlr-in-place-")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tempFileName := handle.Name()
|
|
|
|
// If the input file is compressed and we'll be doing in-process
|
|
// decompression as we read the input file, try to do in-process
|
|
// compression as we write the output.
|
|
inputFileEncoding := lib.FindInputEncoding(fileName, options.ReaderOptions.FileInputEncoding)
|
|
|
|
// Get a handle with, perhaps, a recompression wrapper around it.
|
|
wrappedHandle, isNew, err := lib.WrapOutputHandle(handle, inputFileEncoding)
|
|
if err != nil {
|
|
_ = os.Remove(tempFileName)
|
|
return err
|
|
}
|
|
|
|
// Run the Miller processing stream from the input file to the temp-output file.
|
|
err = stream.Stream([]string{fileName}, options, recordTransformers, wrappedHandle, false)
|
|
if err != nil {
|
|
_ = os.Remove(tempFileName)
|
|
return err
|
|
}
|
|
|
|
// Close the recompressor handle, if any recompression is being applied.
|
|
if isNew {
|
|
err = wrappedHandle.Close()
|
|
if err != nil {
|
|
_ = os.Remove(tempFileName)
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Close the handle to the output file. This may force final writes, so
|
|
// it must be error-checked.
|
|
err = handle.Close()
|
|
if err != nil {
|
|
_ = os.Remove(tempFileName)
|
|
return err
|
|
}
|
|
|
|
// Rename the temp-output file on top of the input file.
|
|
err = os.Rename(tempFileName, fileName)
|
|
if err != nil {
|
|
_ = os.Remove(tempFileName)
|
|
return err
|
|
}
|
|
|
|
// Set the mode to match the original.
|
|
err = os.Chmod(fileName, originalMode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|