// 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) }