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