mirror of
https://github.com/johnkerl/miller.git
synced 2026-07-17 16:38:54 +00:00
* refine the plan
* Fix all staticcheck lint findings (uncapped)
golangci-lint's default max-same-issues=3 was hiding most of the backlog:
the true pre-fix count was 69 staticcheck findings, not 34. This fixes all
of them, driving staticcheck to zero:
- ST1023/QF1011 (37): omit explicit types inferred from the RHS
- S1009/S1031 (15): drop redundant nil checks before len()/range
- SA9003 (9): remove comment-only empty branches, keeping the comments
- QF1007 (3): merge conditional assignment into declaration
- QF1006 (3): lift break conditions into loop conditions
- QF1001 (3): apply De Morgan's law / name the negated predicate
Also updates plans/lintfixes.md with the cap discovery and the corrected
errcheck picture (1202 uncapped, ~949 of them fmt.Fprint*).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Drive errcheck to zero: config for bulk categories, propagate real errors
Adds .golangci.yml with errcheck exclude-functions for fmt.Fprint* (usage
printers), (*bufio.Writer).Write/WriteString (sticky errors, surfaced at the
now-checked final Flush), and (*strings.Builder).WriteString; pins
max-issues-per-linter/max-same-issues to 0 so CI reports true counts.
Real error paths now propagate instead of being dropped:
- Finalize{Reader,Writer}Options in join/put/filter/split/tee and the
repl/script entry points: 'mlr join -i badformat' now errors instead of
silently using wrong separators
- final output-stream Flush in pkg/stream: write failure no longer exits 0
- DSL emit/print/dump redirect writes, matching their sibling branches
- CSV writer WriteCSVRecordMaybeColorized, close-time Flush in file output
handlers, ENV[...] Setenv, REPL record-write and redirect-close errors
- termcvt write-side Close before rename (had "TODO: check return status")
The rest are deliberate ignores, marked with _ = and a comment where the
reason isn't obvious: unset-of-missing-path no-ops, read-side closes,
mid-stream FlushOnEveryRecord, init-time strftime registrations, in-memory
usage-capture pipes, and regtest-harness env/temp-file teardown.
golangci-lint now reports 0 issues on ./cmd/mlr ./pkg/... with all caps off.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
395 lines
13 KiB
Go
395 lines
13 KiB
Go
// This handles print, printn, eprint, and eprintn statements.
|
|
|
|
package cst
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
|
|
"github.com/johnkerl/miller/v6/pkg/lib"
|
|
"github.com/johnkerl/miller/v6/pkg/output"
|
|
"github.com/johnkerl/miller/v6/pkg/runtime"
|
|
"github.com/johnkerl/miller/v6/pkg/types"
|
|
"github.com/johnkerl/pgpg/go/lib/pkg/asts"
|
|
)
|
|
|
|
// Example ASTs:
|
|
//
|
|
// $ mlr -n put -v 'print $a, $b'
|
|
// DSL EXPRESSION:
|
|
// print $a, $b
|
|
// AST:
|
|
// * statement block
|
|
// * print statement "print"
|
|
// * function callsite
|
|
// * direct field value "a"
|
|
// * direct field value "b"
|
|
// * no-op
|
|
//
|
|
// $ mlr -n put -v 'print > stdout, $a, $b'
|
|
// DSL EXPRESSION:
|
|
// print > stdout, $a, $b
|
|
// AST:
|
|
// * statement block
|
|
// * print statement "print"
|
|
// * function callsite
|
|
// * direct field value "a"
|
|
// * direct field value "b"
|
|
// * redirect write ">"
|
|
// * stdout redirect target "stdout"
|
|
//
|
|
// $ mlr -n put -v 'print > stderr, $a, $b'
|
|
// DSL EXPRESSION:
|
|
// print > stderr, $a, $b
|
|
// AST:
|
|
// * statement block
|
|
// * print statement "print"
|
|
// * function callsite
|
|
// * direct field value "a"
|
|
// * direct field value "b"
|
|
// * redirect write ">"
|
|
// * stderr redirect target "stderr"
|
|
//
|
|
// $ mlr -n put -v 'print > "foo.dat", $a, $b'
|
|
// DSL EXPRESSION:
|
|
// print > "foo.dat", $a, $b
|
|
// AST:
|
|
// * statement block
|
|
// * print statement "print"
|
|
// * function callsite
|
|
// * direct field value "a"
|
|
// * direct field value "b"
|
|
// * redirect write ">"
|
|
// * string literal "foo.dat"
|
|
//
|
|
// $ mlr -n put -v 'print >> "foo.dat", $a, $b'
|
|
// DSL EXPRESSION:
|
|
// print >> "foo.dat", $a, $b
|
|
// AST:
|
|
// * statement block
|
|
// * print statement "print"
|
|
// * function callsite
|
|
// * direct field value "a"
|
|
// * direct field value "b"
|
|
// * redirect append ">>"
|
|
// * string literal "foo.dat"
|
|
//
|
|
// $ mlr -n put -v 'print | "command", $a, $b'
|
|
// DSL EXPRESSION:
|
|
// print | "command", $a, $b
|
|
// AST:
|
|
// * statement block
|
|
// * print statement "print"
|
|
// * function callsite
|
|
// * direct field value "a"
|
|
// * direct field value "b"
|
|
// * redirect pipe "|"
|
|
// * string literal "command"
|
|
//
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
// Corresponding data structures for these cases:
|
|
//
|
|
// * printToRedirectFunc is either printToStdout, printToStderr, or
|
|
// printToFileOrPipe. Only the third of these takes a non-nil
|
|
// redirectorTargetEvaluable and a non-nil outputHandlerManager.
|
|
//
|
|
// * redirectorTargetEvaluable is nil for stdout or stderr.
|
|
//
|
|
// * The OutputHandlerManager is for file names or commands in >, >> or |.
|
|
// This is because the target for the redirect can vary from one record to
|
|
// the next, e.g. mlr put 'print > $a.txt, $b'. The OutputHandlerManager
|
|
// keeps file-handles for each distinct value of $a.
|
|
//
|
|
// So:
|
|
//
|
|
// * print $a, $b
|
|
// AST redirectorNode = NodeTypeNoOp
|
|
// AST redirectorTargetNode = (none)
|
|
// printToRedirectFunc = printToStdout
|
|
// redirectorTargetEvaluable = nil
|
|
// outputHandlerManager = nil
|
|
//
|
|
// * print > stdout, $a, $b
|
|
// AST redirectorNode = NodeTypeRedirectWrite
|
|
// AST redirectorTargetNode = NodeTypeRedirectTargetStdout
|
|
// printToRedirectFunc = printToStdout
|
|
// redirectorTargetEvaluable = nil
|
|
// outputHandlerManager = nil
|
|
//
|
|
// * print > stderr, $a, $b
|
|
// AST redirectorNode = NodeTypeRedirectWrite
|
|
// AST redirectorTargetNode = NodeTypeRedirectTargetStderr
|
|
// printToRedirectFunc = printToStderr
|
|
// redirectorTargetEvaluable = nil
|
|
// outputHandlerManager = nil
|
|
//
|
|
// * print > "foo.dat", $a, $b
|
|
// AST redirectorNode = NodeTypeRedirectWrite
|
|
// AST redirectorTargetNode = any of various evaluables
|
|
// printToRedirectFunc = printToFileOrPipe
|
|
// redirectorTargetEvaluable = non-nil
|
|
// outputHandlerManager = non-nil
|
|
//
|
|
// * print >> "foo.dat", $a, $b
|
|
// AST redirectorNode = NodeTypeRedirectAppend
|
|
// AST redirectorTargetNode = any of various evaluables
|
|
// printToRedirectFunc = printToFileOrPipe
|
|
// redirectorTargetEvaluable = non-nil
|
|
// outputHandlerManager = non-nil
|
|
//
|
|
// * print | "command", $a, $b
|
|
// AST redirectorNode = NodeTypeRedirectPipe
|
|
// AST redirectorTargetNode = any of various evaluables
|
|
// printToRedirectFunc = printToFileOrPipe
|
|
// redirectorTargetEvaluable = non-nil
|
|
// outputHandlerManager = non-nil
|
|
|
|
type tPrintToRedirectFunc func(
|
|
outputString string,
|
|
state *runtime.State,
|
|
) error
|
|
|
|
type PrintStatementNode struct {
|
|
expressionEvaluables []IEvaluable
|
|
terminator string
|
|
printToRedirectFunc tPrintToRedirectFunc
|
|
redirectorTargetEvaluable IEvaluable // for file/pipe targets
|
|
outputHandlerManager output.OutputHandlerManager // for file/pipe targets
|
|
}
|
|
|
|
func (root *RootNode) BuildPrintStatementNode(astNode *asts.ASTNode) (IExecutable, error) {
|
|
lib.InternalCodingErrorIf(astNode.Type != asts.NodeType(NodeTypePrintStatement))
|
|
return root.buildPrintxStatementNode(
|
|
astNode,
|
|
os.Stdout,
|
|
"\n",
|
|
)
|
|
}
|
|
|
|
func (root *RootNode) BuildPrintnStatementNode(astNode *asts.ASTNode) (IExecutable, error) {
|
|
lib.InternalCodingErrorIf(astNode.Type != asts.NodeType(NodeTypePrintnStatement))
|
|
return root.buildPrintxStatementNode(
|
|
astNode,
|
|
os.Stdout,
|
|
"",
|
|
)
|
|
}
|
|
|
|
func (root *RootNode) BuildEprintStatementNode(astNode *asts.ASTNode) (IExecutable, error) {
|
|
lib.InternalCodingErrorIf(astNode.Type != asts.NodeType(NodeTypeEprintStatement))
|
|
return root.buildPrintxStatementNode(
|
|
astNode,
|
|
os.Stderr,
|
|
"\n",
|
|
)
|
|
}
|
|
|
|
func (root *RootNode) BuildEprintnStatementNode(astNode *asts.ASTNode) (IExecutable, error) {
|
|
lib.InternalCodingErrorIf(astNode.Type != asts.NodeType(NodeTypeEprintnStatement))
|
|
return root.buildPrintxStatementNode(
|
|
astNode,
|
|
os.Stderr,
|
|
"",
|
|
)
|
|
}
|
|
|
|
// Common code for building print/eprint/printn/eprintn nodes
|
|
|
|
func (root *RootNode) buildPrintxStatementNode(
|
|
astNode *asts.ASTNode,
|
|
defaultOutputStream *os.File,
|
|
terminator string,
|
|
) (IExecutable, error) {
|
|
// Normalize PGPG AST to 2-child layout (expressions, redirector).
|
|
// PGPG produces: 0 children (print), 1 child (print s or print > file), 2+ children (print s,t or print > file, s).
|
|
// With "print Redirector comma FcnArgs", children are [Redirector, FcnArgs].
|
|
// With "print FcnArgs" (with_adopted_grandchildren), children are the Rvalues directly.
|
|
var expressionsNode, redirectorNode *asts.ASTNode
|
|
isRedirector := func(n *asts.ASTNode) bool {
|
|
return n.Type == asts.NodeType(NodeTypeRedirectWrite) ||
|
|
n.Type == asts.NodeType(NodeTypeRedirectAppend) ||
|
|
n.Type == asts.NodeType(NodeTypeRedirectPipe)
|
|
}
|
|
switch len(astNode.Children) {
|
|
case 0:
|
|
expressionsNode = asts.NewASTNode(nil, asts.NodeType(NodeTypeNoOp), nil)
|
|
redirectorNode = asts.NewASTNode(nil, asts.NodeType(NodeTypeNoOp), nil)
|
|
case 1:
|
|
if isRedirector(astNode.Children[0]) {
|
|
expressionsNode = asts.NewASTNode(nil, asts.NodeType(NodeTypeNoOp), nil)
|
|
redirectorNode = astNode.Children[0]
|
|
} else {
|
|
expressionsNode = asts.NewASTNode(nil, asts.NodeType(NodeTypeFcnArgs), astNode.Children)
|
|
redirectorNode = asts.NewASTNode(nil, asts.NodeType(NodeTypeNoOp), nil)
|
|
}
|
|
default:
|
|
if isRedirector(astNode.Children[0]) {
|
|
// print Redirector comma FcnArgs -> [Redirector, FcnArgs]
|
|
expressionsNode = astNode.Children[1]
|
|
redirectorNode = astNode.Children[0]
|
|
} else {
|
|
// print FcnArgs with_adopted_grandchildren -> [Rvalue, Rvalue, ...]
|
|
expressionsNode = asts.NewASTNode(nil, asts.NodeType(NodeTypeFcnArgs), astNode.Children)
|
|
redirectorNode = asts.NewASTNode(nil, asts.NodeType(NodeTypeNoOp), nil)
|
|
}
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
// Things to be printed, e.g. $a and $b in 'print > "foo.dat", $a, $b'.
|
|
|
|
var expressionEvaluables []IEvaluable = nil
|
|
|
|
if expressionsNode.Type == asts.NodeType(NodeTypeNoOp) {
|
|
// Just 'print' without 'print $something'
|
|
expressionEvaluables = make([]IEvaluable, 1)
|
|
expressionEvaluable := root.BuildStringLiteralNode("")
|
|
expressionEvaluables[0] = expressionEvaluable
|
|
} else if expressionsNode.Type == asts.NodeType(NodeTypeFunctionCallsite) ||
|
|
expressionsNode.Type == asts.NodeType(NodeTypeFcnArgs) {
|
|
expressionEvaluables = make([]IEvaluable, len(expressionsNode.Children))
|
|
for i, childNode := range expressionsNode.Children {
|
|
expressionEvaluable, err := root.BuildEvaluableNode(childNode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
expressionEvaluables[i] = expressionEvaluable
|
|
}
|
|
} else {
|
|
lib.InternalCodingErrorIf(true)
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
// Redirection targets (the thing after > >> |, if any).
|
|
|
|
retval := &PrintStatementNode{
|
|
expressionEvaluables: expressionEvaluables,
|
|
terminator: terminator,
|
|
printToRedirectFunc: nil,
|
|
redirectorTargetEvaluable: nil,
|
|
outputHandlerManager: nil,
|
|
}
|
|
|
|
if redirectorNode.Type == asts.NodeType(NodeTypeNoOp) {
|
|
// No > >> or | was provided.
|
|
switch defaultOutputStream {
|
|
case os.Stdout:
|
|
retval.printToRedirectFunc = retval.printToStdout
|
|
case os.Stderr:
|
|
retval.printToRedirectFunc = retval.printToStderr
|
|
default:
|
|
lib.InternalCodingErrorIf(true)
|
|
}
|
|
} else {
|
|
// There is > >> or | provided.
|
|
lib.InternalCodingErrorIf(redirectorNode.Children == nil)
|
|
lib.InternalCodingErrorIf(len(redirectorNode.Children) != 1)
|
|
redirectorTargetNode := redirectorNode.Children[0]
|
|
var err error
|
|
|
|
if redirectorTargetNode.Type == asts.NodeType(NodeTypeRedirectTargetStdout) {
|
|
retval.printToRedirectFunc = retval.printToStdout
|
|
} else if redirectorTargetNode.Type == asts.NodeType(NodeTypeRedirectTargetStderr) {
|
|
retval.printToRedirectFunc = retval.printToStderr
|
|
} else {
|
|
retval.printToRedirectFunc = retval.printToFileOrPipe
|
|
// RedirectTargetRvalue wraps Rvalue; unwrap to build the expression
|
|
targetNode := redirectorTargetNode
|
|
if redirectorTargetNode.Type == asts.NodeType(NodeTypeRedirectTargetRvalue) &&
|
|
redirectorTargetNode.Children != nil && len(redirectorTargetNode.Children) > 0 {
|
|
targetNode = redirectorTargetNode.Children[0]
|
|
}
|
|
retval.redirectorTargetEvaluable, err = root.BuildEvaluableNode(targetNode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if redirectorNode.Type == asts.NodeType(NodeTypeRedirectWrite) {
|
|
retval.outputHandlerManager = output.NewFileWritetHandlerManager(root.recordWriterOptions)
|
|
} else if redirectorNode.Type == asts.NodeType(NodeTypeRedirectAppend) {
|
|
retval.outputHandlerManager = output.NewFileAppendHandlerManager(root.recordWriterOptions)
|
|
} else if redirectorNode.Type == asts.NodeType(NodeTypeRedirectPipe) {
|
|
retval.outputHandlerManager = output.NewPipeWriteHandlerManager(root.recordWriterOptions)
|
|
} else {
|
|
return nil, fmt.Errorf("unhandled redirector node type %s", string(redirectorNode.Type))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Register this with the CST root node so that open file descriptrs can be
|
|
// closed, etc at end of stream.
|
|
if retval.outputHandlerManager != nil {
|
|
root.RegisterOutputHandlerManager(retval.outputHandlerManager)
|
|
}
|
|
|
|
return retval, nil
|
|
}
|
|
|
|
func (node *PrintStatementNode) Execute(state *runtime.State) (*BlockExitPayload, error) {
|
|
if len(node.expressionEvaluables) == 0 {
|
|
if err := node.printToRedirectFunc(node.terminator, state); err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
// 5x faster than fmt.Print() separately: note that os.Stdout is
|
|
// non-buffered in Go whereas stdout is buffered in C.
|
|
//
|
|
// Minus: we need to do our own buffering for performance.
|
|
//
|
|
// Plus: we never have to worry about forgetting to do fflush(). :)
|
|
var buffer bytes.Buffer
|
|
|
|
for i, expressionEvaluable := range node.expressionEvaluables {
|
|
if i > 0 {
|
|
buffer.WriteString(" ")
|
|
}
|
|
evaluation := expressionEvaluable.Evaluate(state)
|
|
if !evaluation.IsAbsent() {
|
|
buffer.WriteString(evaluation.String())
|
|
}
|
|
}
|
|
buffer.WriteString(node.terminator)
|
|
if err := node.printToRedirectFunc(buffer.String(), state); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (node *PrintStatementNode) printToStdout(
|
|
outputString string,
|
|
state *runtime.State,
|
|
) error {
|
|
// Insert the string into the record-output stream, so that goroutine can
|
|
// print it, resulting in deterministic output-ordering.
|
|
|
|
// The output channel is always non-nil, except for the Miller REPL.
|
|
if state.OutputRecordsAndContexts != nil {
|
|
*state.OutputRecordsAndContexts = append(*state.OutputRecordsAndContexts, types.NewOutputString(outputString, state.Context))
|
|
} else {
|
|
fmt.Print(outputString)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (node *PrintStatementNode) printToStderr(
|
|
outputString string,
|
|
state *runtime.State,
|
|
) error {
|
|
fmt.Fprint(os.Stderr, outputString)
|
|
return nil
|
|
}
|
|
|
|
func (node *PrintStatementNode) printToFileOrPipe(
|
|
outputString string,
|
|
state *runtime.State,
|
|
) error {
|
|
redirectorTarget := node.redirectorTargetEvaluable.Evaluate(state)
|
|
if !redirectorTarget.IsString() {
|
|
return fmt.Errorf("output redirection yielded %s, not string", redirectorTarget.GetTypeName())
|
|
}
|
|
outputFileName := redirectorTarget.String()
|
|
|
|
return node.outputHandlerManager.WriteString(outputString, outputFileName)
|
|
}
|