1 package io.jawk;
2
3 /*-
4 * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
5 * Jawk
6 * ჻჻჻჻჻჻
7 * Copyright (C) 2006 - 2026 MetricsHub
8 * ჻჻჻჻჻჻
9 * This program is free software: you can redistribute it and/or modify
10 * it under the terms of the GNU Lesser General Public License as
11 * published by the Free Software Foundation, either version 3 of the
12 * License, or (at your option) any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Lesser Public License for more details.
18 *
19 * You should have received a copy of the GNU General Lesser Public
20 * License along with this program. If not, see
21 * <http://www.gnu.org/licenses/lgpl-3.0.html>.
22 * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱
23 */
24
25 import java.io.ByteArrayInputStream;
26 import java.io.IOException;
27 import java.io.InputStream;
28 import java.io.OutputStream;
29 import java.io.PrintStream;
30 import java.io.Reader;
31 import java.io.StringReader;
32 import java.nio.charset.StandardCharsets;
33 import java.util.ArrayList;
34 import java.util.Arrays;
35 import java.util.Collection;
36 import java.util.Collections;
37 import java.util.LinkedHashMap;
38 import java.util.List;
39 import java.util.Map;
40 import java.util.Objects;
41 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
42 import io.jawk.backend.AVM;
43 import io.jawk.ext.ExtensionFunction;
44 import io.jawk.ext.ExtensionRegistry;
45 import io.jawk.ext.GawkExtension;
46 import io.jawk.ext.JawkExtension;
47 import io.jawk.frontend.AwkParser;
48 import io.jawk.frontend.AstNode;
49 import io.jawk.jrt.AppendableAwkSink;
50 import io.jawk.jrt.AwkSink;
51 import io.jawk.jrt.InputSource;
52 import io.jawk.jrt.OutputStreamAwkSink;
53 import io.jawk.jrt.StreamInputSource;
54 import io.jawk.util.AwkSettings;
55 import io.jawk.util.ScriptSource;
56
57 /**
58 * Entry point into the parsing, analysis, and execution
59 * of a Jawk script.
60 * This entry point is used both when Jawk is executed as a library and when
61 * invoked from the command line.
62 * <p>
63 * The overall process to execute a Jawk script is as follows:
64 * <ul>
65 * <li>Parse the Jawk script, producing an abstract syntax tree.
66 * <li>Traverse the abstract syntax tree, producing a list of
67 * instruction tuples for the interpreter.
68 * <li>Traverse the list of tuples, providing a runtime which
69 * ultimately executes the Jawk script, <strong>or</strong>
70 * Command-line parameters dictate which action is to take place.
71 * </ul>
72 * Two additional semantic checks on the syntax tree are employed
73 * (both to resolve function calls for defined functions).
74 * As a result, the syntax tree is traversed three times.
75 * And the number of times tuples are traversed is depends
76 * on whether interpretation or compilation takes place.
77 * <p>
78 * The engine does not enable any extensions automatically. Extensions can be
79 * provided programmatically via the {@link Awk#Awk(Collection)} constructors or
80 * via the command line when using the CLI entry point.
81 *
82 * @see io.jawk.backend.AVM
83 * @author Danny Daglas
84 */
85 public class Awk {
86
87 /** POSIX default field separator ({@code " "}). */
88 public static final String DEFAULT_FS = " ";
89
90 /** POSIX default record separator ({@code "\n"}). */
91 public static final String DEFAULT_RS = "\n";
92
93 /** POSIX default output field separator ({@code " "}). */
94 public static final String DEFAULT_OFS = " ";
95
96 /** POSIX default output record separator ({@code "\n"}). */
97 public static final String DEFAULT_ORS = "\n";
98
99 /** POSIX default number-to-string conversion format ({@code "%.6g"}). */
100 public static final String DEFAULT_CONVFMT = "%.6g";
101
102 /** POSIX default output number format ({@code "%.6g"}). */
103 public static final String DEFAULT_OFMT = "%.6g";
104
105 /** POSIX default subscript separator ({@code "\034"}). */
106 public static final String DEFAULT_SUBSEP = String.valueOf((char) 28);
107
108 private final Map<String, ExtensionFunction> extensionFunctions;
109
110 private final Map<String, JawkExtension> extensionInstances;
111
112 /**
113 * The behavioral settings used by this engine instance.
114 */
115 private final AwkSettings settings;
116
117 /**
118 * The last parsed {@link AstNode} produced during compilation.
119 */
120 private AstNode lastAst;
121
122 /**
123 * Create a new instance of Awk with default extensions.
124 */
125 public Awk() {
126 this(new AwkSettings());
127 }
128
129 /**
130 * Create a new instance of Awk with the specified settings.
131 *
132 * @param settings behavioral configuration for this engine
133 */
134 public Awk(AwkSettings settings) {
135 this(ExtensionSetup.createDefault(), settings);
136 }
137
138 /**
139 * Create a new instance of Awk with the specified extension instances.
140 *
141 * @param extensions extension instances implementing {@link JawkExtension}
142 */
143 public Awk(Collection<? extends JawkExtension> extensions) {
144 this(createExtensionSetup(extensions));
145 }
146
147 /**
148 * Create a new instance of Awk with the specified extension instances
149 * and settings.
150 *
151 * @param extensions extension instances implementing {@link JawkExtension}
152 * @param settings behavioral configuration for this engine
153 */
154 public Awk(Collection<? extends JawkExtension> extensions, AwkSettings settings) {
155 this(createExtensionSetup(extensions), settings);
156 }
157
158 /**
159 * Create a new instance of Awk with the specified extension instances.
160 *
161 * @param extensions extension instances implementing {@link JawkExtension}
162 */
163 @SafeVarargs
164 public Awk(JawkExtension... extensions) {
165 this(createExtensionSetup(Arrays.asList(extensions)));
166 }
167
168 /**
169 * Creates an engine from an already resolved extension set, with default
170 * settings.
171 * <p>
172 * The public constructors that take no settings delegate here. The extension
173 * set is a private type, so this constructor is private too: a subclass could
174 * not name the parameter type anyway, and selects its extensions through the
175 * public constructors instead.
176 * </p>
177 *
178 * @param setup extension functions and instances to bind to this engine
179 */
180 private Awk(ExtensionSetup setup) {
181 this(setup, new AwkSettings());
182 }
183
184 /**
185 * Creates an engine from an already resolved extension set and the given
186 * settings. Every other constructor ends up here.
187 *
188 * @param setup extension functions and instances to bind to this engine
189 * @param settings behavioral configuration for this engine
190 * @throws NullPointerException if {@code settings} is {@code null}
191 */
192 private Awk(ExtensionSetup setup, AwkSettings settings) {
193 this.extensionFunctions = setup.functions;
194 this.extensionInstances = setup.instances;
195 this.settings = Objects.requireNonNull(settings, "settings");
196 }
197
198 /**
199 * Returns the extension functions bound to this engine, keyed by the Awk
200 * function name that calls them.
201 *
202 * @return an unmodifiable map of Awk function name to implementation
203 */
204 protected Map<String, ExtensionFunction> getExtensionFunctions() {
205 return extensionFunctions;
206 }
207
208 /**
209 * Returns the extension instances bound to this engine, keyed by the fully
210 * qualified name of their class, which is how the interpreter looks them up
211 * when a tuple invokes an extension function.
212 * <p>
213 * Subclasses pass this map on when they build their own interpreter, as
214 * {@link SandboxedAwk} does.
215 * </p>
216 *
217 * @return an unmodifiable map of extension class name to extension instance
218 */
219 protected Map<String, JawkExtension> getExtensionInstances() {
220 return extensionInstances;
221 }
222
223 /**
224 * Returns the behavioral settings associated with this engine instance.
225 *
226 * @return the {@link AwkSettings} used by this instance, never {@code null}
227 */
228 @SuppressFBWarnings("EI_EXPOSE_REP")
229 public AwkSettings getSettings() {
230 return settings;
231 }
232
233 static Map<String, ExtensionFunction> createExtensionFunctionMap(Collection<? extends JawkExtension> extensions) {
234 return createExtensionSetup(extensions).functions;
235 }
236
237 static Map<String, JawkExtension> createExtensionInstanceMap(Collection<? extends JawkExtension> extensions) {
238 return createExtensionSetup(extensions).instances;
239 }
240
241 static Map<String, ExtensionFunction> createExtensionFunctionMap(JawkExtension... extensions) {
242 return createExtensionFunctionMap(
243 extensions == null ? Collections.<JawkExtension>emptyList() : Arrays.asList(extensions));
244 }
245
246 static Map<String, JawkExtension> createExtensionInstanceMap(JawkExtension... extensions) {
247 return createExtensionInstanceMap(
248 extensions == null ? Collections.<JawkExtension>emptyList() : Arrays.asList(extensions));
249 }
250
251 /*
252 * An explicit extension list is honored verbatim, including an empty one:
253 * a caller that passes no extensions gets none, which is the only way to
254 * reclaim names such as gensub or typeof. The default set is installed only
255 * by the no-argument constructors, which route through createDefault().
256 */
257 private static ExtensionSetup createExtensionSetup(Collection<? extends JawkExtension> extensions) {
258 if (extensions == null || extensions.isEmpty()) {
259 return ExtensionSetup.EMPTY;
260 }
261 Map<String, ExtensionFunction> keywordMap = new LinkedHashMap<String, ExtensionFunction>();
262 Map<String, JawkExtension> instanceMap = new LinkedHashMap<String, JawkExtension>();
263 for (JawkExtension extension : extensions) {
264 if (extension == null) {
265 throw new IllegalArgumentException("Extension instance must not be null");
266 }
267 String className = extension.getClass().getName();
268 JawkExtension previousInstance = instanceMap.putIfAbsent(className, extension);
269 if (previousInstance != null) {
270 throw new IllegalArgumentException(
271 "Extension class '" + className + "' was provided multiple times");
272 }
273 for (Map.Entry<String, ExtensionFunction> entry : extension.getExtensionFunctions().entrySet()) {
274 String keyword = entry.getKey();
275 ExtensionFunction previous = keywordMap.putIfAbsent(keyword, entry.getValue());
276 if (previous != null) {
277 throw new IllegalArgumentException(
278 "Keyword '" + keyword + "' already provided by another extension");
279 }
280 }
281 }
282 return new ExtensionSetup(
283 Collections.unmodifiableMap(keywordMap),
284 Collections.unmodifiableMap(instanceMap));
285 }
286
287 private static final class ExtensionSetup {
288
289 private static final ExtensionSetup EMPTY = new ExtensionSetup(
290 Collections.<String, ExtensionFunction>emptyMap(),
291 Collections.<String, JawkExtension>emptyMap());
292
293 /*
294 * Extensions keep per-engine runtime state (VariableManager, JRT), so the
295 * default set must be a fresh instance per Awk engine, never a shared
296 * singleton: two engines sharing one GawkExtension would clobber each
297 * other's runtime bindings.
298 */
299 private static ExtensionSetup createDefault() {
300 return createExtensionSetup(Collections.singletonList(new GawkExtension()));
301 }
302
303 private final Map<String, ExtensionFunction> functions;
304 private final Map<String, JawkExtension> instances;
305
306 private ExtensionSetup(Map<String, ExtensionFunction> functionsParam,
307 Map<String, JawkExtension> instancesParam) {
308 this.functions = functionsParam;
309 this.instances = instancesParam;
310 }
311 }
312
313 /**
314 * Returns the last parsed AST produced by the most recent program compilation.
315 *
316 * @return the last {@link AstNode}, or {@code null} if no compilation occurred
317 */
318 @SuppressFBWarnings("EI_EXPOSE_REP")
319 public AstNode getLastAst() {
320 return lastAst;
321 }
322
323 /**
324 * Final empty finalizer to mitigate finalizer attacks flagged by SpotBugs.
325 * This prevents subclasses from introducing a finalizer that could run on a
326 * partially constructed instance if a constructor throws.
327 */
328 @SuppressWarnings("deprecation")
329 @Override
330 protected final void finalize() { /* no-op */ }
331
332 /**
333 * Compiles a full AWK program.
334 *
335 * @param script AWK program source
336 * @return compiled immutable program
337 * @throws IOException if compilation fails
338 */
339 public AwkProgram compile(String script) throws IOException {
340 return compile(script, false);
341 }
342
343 /**
344 * Compiles a full AWK program.
345 *
346 * @param script AWK program source
347 * @return compiled immutable program
348 * @throws IOException if compilation fails
349 */
350 public AwkProgram compile(Reader script) throws IOException {
351 return compile(script, false);
352 }
353
354 /**
355 * Creates a reusable runtime backed by one {@link AVM} instance.
356 *
357 * @return reusable AVM
358 */
359 public AVM createAvm() {
360 return createAvm(this.settings);
361 }
362
363 /**
364 * Creates a reusable runtime backed by one {@link AVM} instance, optionally
365 * collecting runtime profiling statistics.
366 *
367 * @param profilingEnabled whether runtime profiling should be enabled
368 * @return reusable AVM
369 */
370 public AVM createAvm(boolean profilingEnabled) {
371 return createAvm(this.settings, profilingEnabled);
372 }
373
374 /**
375 * Starts building a run request for a compiled AWK program.
376 * <p>
377 * Use the returned {@link AwkRunBuilder} to configure input, arguments,
378 * variables, and output, then call one of the terminal methods to execute.
379 * </p>
380 *
381 * <pre>{@code
382 * awk.script(program).input(stream).execute(mySink);
383 * String out = awk.script(program).input("hello").execute();
384 * }</pre>
385 *
386 * @param program compiled program to execute
387 * @return a builder for configuring and executing the run
388 */
389 public AwkRunBuilder script(AwkProgram program) {
390 return new AwkRunBuilder(Objects.requireNonNull(program, "program"));
391 }
392
393 /**
394 * Starts building a run request from an AWK script string.
395 * <p>
396 * The script is compiled and executed when a terminal method is called.
397 * Additional scripts can be appended by calling {@link AwkRunBuilder#script(String)}
398 * on the returned builder.
399 * </p>
400 *
401 * <pre>{@code
402 * String result = awk.script("{ print toupper($0) }").input("hello").execute();
403 * }</pre>
404 *
405 * @param scriptText AWK program source
406 * @return a builder for configuring and executing the run
407 */
408 public AwkRunBuilder script(String scriptText) {
409 return new AwkRunBuilder().script(Objects.requireNonNull(scriptText, "script"));
410 }
411
412 /**
413 * Evaluates a compiled expression using a fresh isolated runtime.
414 *
415 * @param expression compiled expression
416 * @return evaluated value
417 * @throws IOException if evaluation fails
418 */
419 public Object eval(AwkExpression expression) throws IOException {
420 AwkExpression compiledExpression = Objects.requireNonNull(expression, "expression");
421 try (AVM activeEvalAvm = createAvm(settings)) {
422 return activeEvalAvm.eval(compiledExpression, new SingleRecordInputSource(null));
423 }
424 }
425
426 /**
427 * Evaluates a compiled expression against one text record using a fresh
428 * isolated runtime.
429 *
430 * @param expression compiled expression
431 * @param input record exposed as {@code $0}
432 * @return evaluated value
433 * @throws IOException if evaluation fails
434 */
435 public Object eval(AwkExpression expression, String input) throws IOException {
436 AwkExpression compiledExpression = Objects.requireNonNull(expression, "expression");
437 try (AVM activeEvalAvm = createAvm(settings)) {
438 return activeEvalAvm.eval(compiledExpression, new SingleRecordInputSource(input));
439 }
440 }
441
442 /**
443 * Evaluates a compiled expression against one structured record source using a
444 * fresh isolated runtime.
445 *
446 * @param expression compiled expression
447 * @param source structured record source
448 * @return evaluated value
449 * @throws IOException if evaluation fails
450 */
451 public Object eval(AwkExpression expression, InputSource source) throws IOException {
452 AwkExpression compiledExpression = Objects.requireNonNull(expression, "expression");
453 InputSource resolvedSource = Objects.requireNonNull(source, "source");
454 try (AVM activeEvalAvm = createAvm(settings)) {
455 return activeEvalAvm.eval(compiledExpression, resolvedSource);
456 }
457 }
458
459 /**
460 * Compiles the specified AWK script and returns an immutable AWK program.
461 *
462 * @param script AWK script to compile
463 * @param disableOptimizeParam {@code true} to skip tuple optimization
464 * @return compiled immutable program
465 * @throws IOException if an I/O error occurs during compilation
466 */
467 AwkProgram compile(String script, boolean disableOptimizeParam) throws IOException {
468 ScriptSource source = new ScriptSource(
469 ScriptSource.DESCRIPTION_COMMAND_LINE_SCRIPT,
470 new StringReader(script));
471 return compile(Collections.singletonList(source), disableOptimizeParam);
472 }
473
474 /**
475 * Compiles the specified AWK script and returns an immutable AWK program.
476 *
477 * @param script AWK script to compile (as a {@link Reader})
478 * @param disableOptimizeParam {@code true} to skip tuple optimization
479 * @return compiled immutable program
480 * @throws IOException if an I/O error occurs during compilation
481 */
482 AwkProgram compile(Reader script, boolean disableOptimizeParam) throws IOException {
483 ScriptSource source = new ScriptSource(
484 ScriptSource.DESCRIPTION_COMMAND_LINE_SCRIPT,
485 script);
486 return compile(Collections.singletonList(source), disableOptimizeParam);
487 }
488
489 /**
490 * Compiles a list of script sources into an immutable AWK program that can be
491 * executed by the {@link AVM} runtime.
492 *
493 * @param scripts script sources to compile
494 * @return compiled immutable program
495 * @throws IOException if an I/O error occurs while reading the
496 * scripts
497 */
498 public AwkProgram compile(List<ScriptSource> scripts)
499 throws IOException {
500 return compile(scripts, false);
501 }
502
503 /**
504 * Compiles a list of script sources into an immutable AWK program that can be
505 * executed by the {@link AVM} runtime.
506 *
507 * @param scripts script sources to compile
508 * @param disableOptimizeParam {@code true} to skip tuple optimization
509 * @return compiled immutable program
510 * @throws IOException if an I/O error occurs while reading the
511 * scripts
512 */
513 public AwkProgram compile(List<ScriptSource> scripts, boolean disableOptimizeParam)
514 throws IOException {
515 return compileProgram(scripts, disableOptimizeParam, new AwkProgram());
516 }
517
518 /**
519 * Compiles a full AWK program into the supplied tuple implementation.
520 *
521 * @param scripts script sources to compile
522 * @param disableOptimizeParam {@code true} to skip tuple optimization
523 * @param tuples destination tuple implementation
524 * @param <T> concrete tuple type to populate
525 * @return the populated compiled program
526 * @throws IOException if reading script sources fails
527 */
528 protected final <T extends AwkProgram> T compileProgram(
529 List<ScriptSource> scripts,
530 boolean disableOptimizeParam,
531 T tuples)
532 throws IOException {
533 lastAst = null;
534 if (!scripts.isEmpty()) {
535 // Parse all script sources into a single AST
536 AwkParser parser = new AwkParser(
537 this.extensionFunctions,
538 settings.isPosix(),
539 isSourceIncludeAllowed());
540 AstNode ast = parser.parse(scripts);
541 lastAst = ast;
542 if (ast != null) {
543 // Perform semantic checks twice to resolve forward references
544 ast.semanticAnalysis();
545 ast.semanticAnalysis();
546 // Record the primary source description for runtime diagnostics
547 tuples.setSourceDescription(scripts.get(0).getDescription());
548 // Build tuples from the AST
549 ast.populateTuples(tuples);
550 // Assign addresses and prepare tuples for interpretation
551 tuples.postProcess();
552 if (!disableOptimizeParam) {
553 tuples.optimize();
554 }
555 // Record global variable offset mappings for the interpreter
556 parser.populateGlobalVariableNameToOffsetMappings(tuples);
557 }
558 }
559 tuples.freezeMetadata();
560
561 return tuples;
562 }
563
564 /**
565 * Returns whether scripts compiled by this engine may use {@code @include}.
566 *
567 * @return {@code true} for the standard engine
568 */
569 protected boolean isSourceIncludeAllowed() {
570 return true;
571 }
572
573 /**
574 * Compile an expression to evaluate (not a full script).
575 *
576 * @param expression AWK expression to compile
577 * @return compiled immutable expression
578 * @throws IOException if anything goes wrong with the compilation
579 */
580 public AwkExpression compileExpression(String expression) throws IOException {
581 return compileExpression(expression, false);
582 }
583
584 /**
585 * Compile an expression to evaluate (not a full script).
586 *
587 * @param expression AWK expression to compile
588 * @param disableOptimizeParam {@code true} to skip tuple optimization
589 * @return compiled immutable expression
590 * @throws IOException if anything goes wrong with the compilation
591 */
592 public AwkExpression compileExpression(String expression, boolean disableOptimizeParam) throws IOException {
593 return compileExpression(expression, disableOptimizeParam, new AwkExpression());
594 }
595
596 /**
597 * Compiles an AWK expression into the supplied tuple implementation.
598 *
599 * @param expression expression source to compile
600 * @param disableOptimizeParam {@code true} to skip tuple optimization
601 * @param tuples destination tuple implementation
602 * @param <T> concrete tuple type to populate
603 * @return the populated compiled expression
604 * @throws IOException if reading the expression fails
605 */
606 protected final <T extends AwkExpression> T compileExpression(
607 String expression,
608 boolean disableOptimizeParam,
609 T tuples)
610 throws IOException {
611 // Create a ScriptSource
612 ScriptSource expressionSource = new ScriptSource(
613 ScriptSource.DESCRIPTION_COMMAND_LINE_SCRIPT,
614 new StringReader(expression));
615
616 // Parse the expression
617 AwkParser parser = new AwkParser(this.extensionFunctions, settings.isPosix());
618 AstNode ast = parser.parseExpression(expressionSource);
619
620 // Attempt to traverse the syntax tree and build
621 // the intermediate code
622 if (ast != null) {
623 // 1st pass to tie actual parameters to back-referenced formal parameters
624 ast.semanticAnalysis();
625 // 2nd pass to tie actual parameters to forward-referenced formal parameters
626 ast.semanticAnalysis();
627 // build tuples
628 ast.populateTuples(tuples);
629 // Calls touch(...) per Tuple so that addresses can be normalized/assigned/allocated
630 tuples.postProcess();
631 if (!disableOptimizeParam) {
632 tuples.optimize();
633 }
634 // record global_var -> offset mapping into the tuples
635 // so that the interpreter can assign variables
636 parser.populateGlobalVariableNameToOffsetMappings(tuples);
637 }
638 tuples.freezeMetadata();
639
640 return tuples;
641 }
642
643 /**
644 * Evaluates the specified AWK expression (not a full script, just an expression)
645 * and returns the value of this expression.
646 *
647 * @param expression Expression to evaluate (e.g. <code>2+3</code>)
648 * @return the value of the specified expression
649 * @throws IOException if anything goes wrong with the evaluation
650 */
651 public Object eval(String expression) throws IOException {
652 return eval(compileExpression(expression));
653 }
654
655 /**
656 * Evaluates the specified AWK expression (not a full script, just an expression)
657 * and returns the value of this expression.
658 *
659 * @param expression Expression to evaluate (e.g. <code>2+3</code> or <code>$2 "-" $3</code>
660 * @param input Optional text input (that will be available as $0, and tokenized as $1, $2, etc.)
661 * @return the value of the specified expression
662 * @throws IOException if anything goes wrong with the evaluation
663 */
664 public Object eval(String expression, String input) throws IOException {
665 return eval(compileExpression(expression), input);
666 }
667
668 /**
669 * Evaluates the specified AWK expression using a structured {@link InputSource}
670 * to populate {@code $0}, {@code $1}, etc.
671 *
672 * @param expression Expression to evaluate (e.g. {@code $2 "-" $3})
673 * @param source structured input source providing the current record
674 * @return the value of the specified expression
675 * @throws IOException if anything goes wrong with the evaluation
676 */
677 public Object eval(String expression, InputSource source) throws IOException {
678 return eval(compileExpression(expression), source);
679 }
680
681 /**
682 * Prepares one text record for repeated expression evaluation and returns the
683 * mutable {@link AVM} that will execute those expressions.
684 * <p>
685 * The returned {@link AVM} is created using the current runtime
686 * configuration of this {@link Awk} instance and binds the provided record
687 * once. Later calls to
688 * {@link AVM#eval(AwkExpression)} reuse the same AVM state without resetting it
689 * between expressions, so mutations intentionally leak across evaluations.
690 * This is the high-level convenience wrapper around direct
691 * {@link AVM#prepareForEval(String)} and {@link AVM#eval(AwkExpression)} usage.
692 * </p>
693 *
694 * @param input non-null text record to expose as {@code $0}
695 * Call {@link AVM#close()} when you are done with the returned interpreter.
696 * @return prepared AVM ready for repeated {@link AVM#eval(AwkExpression)} calls
697 * @throws IOException if binding the record fails
698 */
699 public AVM prepareEval(String input) throws IOException {
700 String resolvedInput = Objects.requireNonNull(input, "input");
701 AVM evalAvm = createAvm(settings);
702 try {
703 evalAvm.prepareForEval(resolvedInput);
704 return evalAvm;
705 } catch (IOException | RuntimeException e) {
706 try {
707 evalAvm.close();
708 } catch (IOException closeException) {
709 e.addSuppressed(closeException);
710 }
711 throw e;
712 }
713 }
714
715 /**
716 * Prepares the first available record from a structured {@link InputSource}
717 * for repeated expression evaluation and returns the mutable {@link AVM}
718 * that will execute those expressions.
719 * <p>
720 * The returned AVM remains attached to the provided source, so later
721 * {@code getline} operations and repeated {@link AVM#prepareForEval(InputSource)}
722 * calls continue from that source's current position. Later
723 * {@link AVM#eval(AwkExpression)} calls reuse the same AVM state without
724 * resetting it between expressions, so mutations intentionally leak across
725 * evaluations. Close the returned AVM when you are done with it to release
726 * any bound input or runtime I/O resources.
727 * </p>
728 *
729 * @param source structured source providing the record to bind
730 * @return prepared AVM ready for repeated {@link AVM#eval(AwkExpression)} calls
731 * @throws IOException if reading the record fails or the source is exhausted
732 */
733 public AVM prepareEval(InputSource source) throws IOException {
734 InputSource resolvedSource = Objects.requireNonNull(source, "source");
735 AVM evalAvm = createAvm(settings);
736 try {
737 if (!evalAvm.prepareForEval(resolvedSource)) {
738 throw new IOException("No record available from source.");
739 }
740 return evalAvm;
741 } catch (IOException | RuntimeException e) {
742 try {
743 evalAvm.close();
744 } catch (IOException closeException) {
745 e.addSuppressed(closeException);
746 }
747 throw e;
748 }
749 }
750
751 /**
752 * Creates an {@link AVM} using the provided runtime settings.
753 *
754 * @param settingsParam runtime settings to apply
755 * @return reusable AVM
756 */
757 protected AVM createAvm(AwkSettings settingsParam) {
758 return createAvm(settingsParam, false);
759 }
760
761 /**
762 * Creates an {@link AVM} using the provided runtime settings and profiling
763 * mode.
764 *
765 * @param settingsParam runtime settings to apply
766 * @param profilingEnabled whether runtime profiling should be enabled
767 * @return reusable AVM
768 */
769 protected AVM createAvm(AwkSettings settingsParam, boolean profilingEnabled) {
770 return new AVM(settingsParam, this.extensionInstances, profilingEnabled);
771 }
772
773 /**
774 * Converts a text input into an {@link InputStream} using UTF-8 encoding.
775 */
776 private static InputStream toInputStream(String input) {
777 if (input == null) {
778 return new ByteArrayInputStream(new byte[0]);
779 }
780 return new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8));
781 }
782
783 /**
784 * Fluent builder for configuring and executing an AWK script or program.
785 * <p>
786 * Obtain an instance through {@link Awk#script(String)} or
787 * {@link Awk#script(AwkProgram)}, configure input, arguments, and
788 * variables, then call one of the terminal methods to execute.
789 * </p>
790 *
791 * <pre>{@code
792 * // Execute and capture printed output as a String
793 * String result = awk.script("{ print toupper($0) }").input("hello").execute();
794 *
795 * // Execute to a specific stream
796 * awk.script(program).input(stream).execute(outputStream);
797 *
798 * // Execute with a custom sink
799 * awk.script("{ print $1 }").input(source).execute(mySink);
800 *
801 * // Execute to an appendable
802 * awk.script("{ print $1 }").input(source).execute(appendable);
803 * }</pre>
804 */
805 public final class AwkRunBuilder {
806
807 private AwkProgram compiledProgram;
808 private List<String> scripts;
809 private InputStream inputStream;
810 private InputSource inputSource;
811 private List<String> arguments;
812 private Map<String, Object> variableOverrides;
813 private PrintStream errorStream;
814
815 AwkRunBuilder() {}
816
817 AwkRunBuilder(AwkProgram program) {
818 this.compiledProgram = program;
819 }
820
821 /**
822 * Appends an additional AWK script to compile and execute.
823 * Multiple scripts are concatenated, like multiple {@code -f} options
824 * in the CLI.
825 *
826 * @param scriptText AWK program source
827 * @return this builder
828 * @throws IllegalStateException if a precompiled program was already set
829 */
830 public AwkRunBuilder script(String scriptText) {
831 if (compiledProgram != null) {
832 throw new IllegalStateException("Cannot add scripts when a precompiled program is set");
833 }
834 if (scripts == null) {
835 scripts = new ArrayList<String>();
836 }
837 scripts.add(Objects.requireNonNull(scriptText, "script"));
838 return this;
839 }
840
841 /**
842 * Sets the text input to process.
843 *
844 * @param input text input (encoded as UTF-8 internally)
845 * @return this builder
846 */
847 public AwkRunBuilder input(String input) {
848 this.inputStream = toInputStream(input);
849 return this;
850 }
851
852 /**
853 * Sets the byte-stream input to process.
854 *
855 * @param input byte stream, or {@code null} for no input
856 * @return this builder
857 */
858 public AwkRunBuilder input(InputStream input) {
859 this.inputStream = input;
860 return this;
861 }
862
863 /**
864 * Sets a structured {@link InputSource} to process.
865 *
866 * @param source structured record source
867 * @return this builder
868 */
869 public AwkRunBuilder input(InputSource source) {
870 this.inputSource = source;
871 return this;
872 }
873
874 /**
875 * Sets runtime arguments visible through {@code ARGC}/{@code ARGV}.
876 *
877 * @param args runtime arguments
878 * @return this builder
879 */
880 @SuppressFBWarnings("EI_EXPOSE_REP2")
881 public AwkRunBuilder arguments(List<String> args) {
882 this.arguments = args;
883 return this;
884 }
885
886 /**
887 * Sets runtime arguments visible through {@code ARGC}/{@code ARGV}.
888 *
889 * @param args runtime arguments
890 * @return this builder
891 */
892 public AwkRunBuilder arguments(String... args) {
893 this.arguments = Arrays.asList(args);
894 return this;
895 }
896
897 /**
898 * Adds a single runtime argument visible through {@code ARGC}/{@code ARGV}.
899 *
900 * @param arg runtime argument
901 * @return this builder
902 */
903 public AwkRunBuilder argument(String arg) {
904 if (this.arguments == null) {
905 this.arguments = new ArrayList<String>();
906 }
907 this.arguments.add(Objects.requireNonNull(arg, "arg"));
908 return this;
909 }
910
911 /**
912 * Sets the stream used for the stderr output of spawned processes
913 * (e.g. {@code system("...")}).
914 * <p>
915 * When not set, process stderr is merged into the main output sink.
916 * The CLI sets this explicitly to {@code System.err} so that command
917 * errors appear on the console rather than being mixed with normal output.
918 *
919 * @param stream stream to receive process stderr
920 * @return this builder
921 */
922 public AwkRunBuilder errorStream(PrintStream stream) {
923 this.errorStream = Objects.requireNonNull(stream, "errorStream");
924 return this;
925 }
926
927 /**
928 * Sets per-call variable overrides applied on top of the settings-level
929 * variables.
930 *
931 * @param overrides variable assignments (may be {@code null})
932 * @return this builder
933 */
934 @SuppressFBWarnings("EI_EXPOSE_REP2")
935 public AwkRunBuilder variables(Map<String, Object> overrides) {
936 this.variableOverrides = overrides;
937 return this;
938 }
939
940 /**
941 * Sets a single per-call variable override.
942 *
943 * @param name variable name
944 * @param value variable value
945 * @return this builder
946 */
947 public AwkRunBuilder variable(String name, Object value) {
948 if (this.variableOverrides == null) {
949 this.variableOverrides = new LinkedHashMap<String, Object>();
950 }
951 this.variableOverrides
952 .put(
953 Objects.requireNonNull(name, "name"),
954 value);
955 return this;
956 }
957
958 /**
959 * Executes the script and returns the printed output as a {@link String}.
960 *
961 * @return printed output
962 * @throws IOException if compilation or execution fails
963 * @throws ExitException if the script terminates with a non-zero exit code
964 */
965 public String execute() throws IOException, ExitException {
966 StringBuilder output = new StringBuilder();
967 doExecute(new AppendableAwkSink(output, settings.getLocale()));
968 return output.toString();
969 }
970
971 /**
972 * Executes the script, sending output to the specified {@link AwkSink}.
973 *
974 * @param sink output sink
975 * @throws IOException if compilation or execution fails
976 * @throws ExitException if the script terminates with a non-zero exit code
977 */
978 public void execute(AwkSink sink) throws IOException, ExitException {
979 doExecute(Objects.requireNonNull(sink, "sink"));
980 }
981
982 /**
983 * Executes the script, sending output to the specified {@link PrintStream}.
984 *
985 * @param out print stream (e.g. {@code System.out})
986 * @throws IOException if compilation or execution fails
987 * @throws ExitException if the script terminates with a non-zero exit code
988 */
989 public void execute(PrintStream out) throws IOException, ExitException {
990 Objects.requireNonNull(out, "out");
991 doExecute(new OutputStreamAwkSink(out, settings.getLocale()));
992 }
993
994 /**
995 * Executes the script, sending output to the specified {@link OutputStream}.
996 *
997 * @param out output stream
998 * @throws IOException if compilation or execution fails
999 * @throws ExitException if the script terminates with a non-zero exit code
1000 */
1001 public void execute(OutputStream out) throws IOException, ExitException {
1002 doExecute(new OutputStreamAwkSink(toPrintStream(out), settings.getLocale()));
1003 }
1004
1005 /**
1006 * Executes the script, sending output to the specified {@link Appendable}
1007 * (such as {@link StringBuilder} or {@link java.io.StringWriter}).
1008 *
1009 * @param appendable output destination
1010 * @throws IOException if compilation or execution fails
1011 * @throws ExitException if the script terminates with a non-zero exit code
1012 */
1013 public void execute(Appendable appendable) throws IOException, ExitException {
1014 doExecute(
1015 new AppendableAwkSink(
1016 Objects.requireNonNull(appendable, "appendable"),
1017 settings.getLocale()));
1018 }
1019
1020 private void doExecute(AwkSink sink) throws IOException, ExitException {
1021 AwkProgram program = resolveProgram();
1022 List<String> resolvedArguments = arguments == null ? Collections.<String>emptyList() : arguments;
1023 try (AVM avm = createAvm(settings)) {
1024 avm.setAwkSink(sink);
1025 if (errorStream != null) {
1026 avm.setErrorStream(errorStream);
1027 avm.setWarningStream(errorStream);
1028 } else {
1029 // process stderr keeps its historical sink fallback, but
1030 // warnings stay on System.err so they can never leak into
1031 // the script output a host captures
1032 avm.setErrorStream(sink.getPrintStream());
1033 }
1034 try {
1035 InputSource resolvedSource;
1036 if (inputSource != null) {
1037 resolvedSource = inputSource;
1038 } else {
1039 InputStream in = inputStream != null ? inputStream : new ByteArrayInputStream(new byte[0]);
1040 resolvedSource = new StreamInputSource(in, avm, avm.getJrt());
1041 }
1042 avm.execute(program, resolvedSource, resolvedArguments, variableOverrides);
1043 } catch (ExitException e) {
1044 if (e.getCode() != 0) {
1045 throw e;
1046 }
1047 } finally {
1048 sink.flush();
1049 }
1050 }
1051 }
1052
1053 private AwkProgram resolveProgram() throws IOException {
1054 if (compiledProgram != null) {
1055 return compiledProgram;
1056 }
1057 if (scripts == null || scripts.isEmpty()) {
1058 throw new IllegalStateException("No script or program specified");
1059 }
1060 if (scripts.size() == 1) {
1061 return compile(scripts.get(0));
1062 }
1063 List<ScriptSource> sources = new ArrayList<ScriptSource>(scripts.size());
1064 for (int i = 0; i < scripts.size(); i++) {
1065 sources
1066 .add(
1067 new ScriptSource(
1068 ScriptSource.DESCRIPTION_COMMAND_LINE_SCRIPT,
1069 new StringReader(scripts.get(i))));
1070 }
1071 return compile(sources);
1072 }
1073 }
1074
1075 private static PrintStream toPrintStream(OutputStream out) {
1076 Objects.requireNonNull(out, "outputStream");
1077 if (out instanceof PrintStream) {
1078 return (PrintStream) out;
1079 }
1080 try {
1081 return new PrintStream(out, false, "UTF-8");
1082 } catch (java.io.UnsupportedEncodingException e) {
1083 throw new IllegalStateException(e);
1084 }
1085 }
1086
1087 /**
1088 * Lists metadata for the {@link JawkExtension} implementations discovered on
1089 * the class path.
1090 *
1091 * @return list of discovered extension descriptors
1092 */
1093 public static Map<String, JawkExtension> listAvailableExtensions() {
1094 return ExtensionRegistry.listExtensions();
1095 }
1096
1097 private static final class SingleRecordInputSource implements InputSource {
1098
1099 private final String record;
1100
1101 private boolean consumed;
1102
1103 private SingleRecordInputSource(String record) {
1104 this.record = record;
1105 }
1106
1107 @Override
1108 public boolean nextRecord() {
1109 if (consumed || record == null) {
1110 return false;
1111 }
1112 consumed = true;
1113 return true;
1114 }
1115
1116 @Override
1117 public String getRecordText() {
1118 return consumed ? record : null;
1119 }
1120
1121 @Override
1122 public List<String> getFields() {
1123 return null;
1124 }
1125
1126 @Override
1127 public boolean isFromFilenameList() {
1128 return false;
1129 }
1130 }
1131
1132 }