1 package org.metricshub.jawk;
2
3 /*-
4 * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
5 * Jawk
6 * ჻჻჻჻჻჻
7 * Copyright (C) 2006 - 2025 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.ByteArrayOutputStream;
27 import java.io.File;
28 import java.io.FileInputStream;
29 import java.io.IOException;
30 import java.io.InputStream;
31 import java.io.OutputStream;
32 import java.io.PrintStream;
33 import java.io.Reader;
34 import java.io.StringReader;
35 import java.nio.charset.StandardCharsets;
36 import java.util.Arrays;
37 import java.util.Collection;
38 import java.util.Collections;
39 import java.util.LinkedHashMap;
40 import java.util.List;
41 import java.util.Map;
42 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
43 import org.metricshub.jawk.backend.AVM;
44 import org.metricshub.jawk.ext.ExtensionFunction;
45 import org.metricshub.jawk.ext.ExtensionRegistry;
46 import org.metricshub.jawk.ext.JawkExtension;
47 import org.metricshub.jawk.frontend.AwkParser;
48 import org.metricshub.jawk.frontend.AstNode;
49 import org.metricshub.jawk.intermediate.AwkTuples;
50 import org.metricshub.jawk.util.AwkSettings;
51 import org.metricshub.jawk.util.ScriptSource;
52
53 /**
54 * Entry point into the parsing, analysis, and execution
55 * of a Jawk script.
56 * This entry point is used both when Jawk is executed as a library and when
57 * invoked from the command line.
58 * <p>
59 * The overall process to execute a Jawk script is as follows:
60 * <ul>
61 * <li>Parse the Jawk script, producing an abstract syntax tree.
62 * <li>Traverse the abstract syntax tree, producing a list of
63 * instruction tuples for the interpreter.
64 * <li>Traverse the list of tuples, providing a runtime which
65 * ultimately executes the Jawk script, <strong>or</strong>
66 * Command-line parameters dictate which action is to take place.
67 * </ul>
68 * Two additional semantic checks on the syntax tree are employed
69 * (both to resolve function calls for defined functions).
70 * As a result, the syntax tree is traversed three times.
71 * And the number of times tuples are traversed is depends
72 * on whether interpretation or compilation takes place.
73 * <p>
74 * The engine does not enable any extensions automatically. Extensions can be
75 * provided programmatically via the {@link Awk#Awk(Collection)} constructors or
76 * via the command line when using the CLI entry point.
77 *
78 * @see org.metricshub.jawk.backend.AVM
79 * @author Danny Daglas
80 */
81 public class Awk {
82
83 private final Map<String, ExtensionFunction> extensionFunctions;
84
85 private final Map<String, JawkExtension> extensionInstances;
86
87 /**
88 * The last parsed {@link AstNode} produced during compilation.
89 */
90 private AstNode lastAst;
91
92 /**
93 * Create a new instance of Awk without extensions
94 */
95 public Awk() {
96 this(ExtensionSetup.EMPTY);
97 }
98
99 /**
100 * Create a new instance of Awk with the specified extension instances.
101 *
102 * @param extensions extension instances implementing {@link JawkExtension}
103 */
104 public Awk(Collection<? extends JawkExtension> extensions) {
105 this(createExtensionSetup(extensions));
106 }
107
108 /**
109 * Create a new instance of Awk with the specified extension instances.
110 *
111 * @param extensions extension instances implementing {@link JawkExtension}
112 */
113 @SafeVarargs
114 public Awk(JawkExtension... extensions) {
115 this(createExtensionSetup(Arrays.asList(extensions)));
116 }
117
118 protected Awk(ExtensionSetup setup) {
119 this.extensionFunctions = setup.functions;
120 this.extensionInstances = setup.instances;
121 }
122
123 protected Map<String, ExtensionFunction> getExtensionFunctions() {
124 return extensionFunctions;
125 }
126
127 protected Map<String, JawkExtension> getExtensionInstances() {
128 return extensionInstances;
129 }
130
131 static Map<String, ExtensionFunction> createExtensionFunctionMap(Collection<? extends JawkExtension> extensions) {
132 return createExtensionSetup(extensions).functions;
133 }
134
135 static Map<String, JawkExtension> createExtensionInstanceMap(Collection<? extends JawkExtension> extensions) {
136 return createExtensionSetup(extensions).instances;
137 }
138
139 static Map<String, ExtensionFunction> createExtensionFunctionMap(JawkExtension... extensions) {
140 if (extensions == null || extensions.length == 0) {
141 return ExtensionSetup.EMPTY.functions;
142 }
143 return createExtensionFunctionMap(Arrays.asList(extensions));
144 }
145
146 static Map<String, JawkExtension> createExtensionInstanceMap(JawkExtension... extensions) {
147 if (extensions == null || extensions.length == 0) {
148 return ExtensionSetup.EMPTY.instances;
149 }
150 return createExtensionInstanceMap(Arrays.asList(extensions));
151 }
152
153 private static ExtensionSetup createExtensionSetup(Collection<? extends JawkExtension> extensions) {
154 if (extensions == null || extensions.isEmpty()) {
155 return ExtensionSetup.EMPTY;
156 }
157 Map<String, ExtensionFunction> keywordMap = new LinkedHashMap<String, ExtensionFunction>();
158 Map<String, JawkExtension> instanceMap = new LinkedHashMap<String, JawkExtension>();
159 for (JawkExtension extension : extensions) {
160 if (extension == null) {
161 throw new IllegalArgumentException("Extension instance must not be null");
162 }
163 String className = extension.getClass().getName();
164 JawkExtension previousInstance = instanceMap.putIfAbsent(className, extension);
165 if (previousInstance != null) {
166 throw new IllegalArgumentException(
167 "Extension class '" + className + "' was provided multiple times");
168 }
169 for (Map.Entry<String, ExtensionFunction> entry : extension.getExtensionFunctions().entrySet()) {
170 String keyword = entry.getKey();
171 ExtensionFunction previous = keywordMap.putIfAbsent(keyword, entry.getValue());
172 if (previous != null) {
173 throw new IllegalArgumentException(
174 "Keyword '" + keyword + "' already provided by another extension");
175 }
176 }
177 }
178 return new ExtensionSetup(
179 Collections.unmodifiableMap(keywordMap),
180 Collections.unmodifiableMap(instanceMap));
181 }
182
183 private static final class ExtensionSetup {
184
185 private static final ExtensionSetup EMPTY = new ExtensionSetup(
186 Collections.<String, ExtensionFunction>emptyMap(),
187 Collections.<String, JawkExtension>emptyMap());
188
189 private final Map<String, ExtensionFunction> functions;
190 private final Map<String, JawkExtension> instances;
191
192 private ExtensionSetup(Map<String, ExtensionFunction> functionsParam,
193 Map<String, JawkExtension> instancesParam) {
194 this.functions = functionsParam;
195 this.instances = instancesParam;
196 }
197 }
198
199 /**
200 * Returns the last parsed AST produced by {@link #compile(List)}.
201 *
202 * @return the last {@link AstNode}, or {@code null} if no compilation occurred
203 */
204 @SuppressFBWarnings("EI_EXPOSE_REP")
205 public AstNode getLastAst() {
206 return lastAst;
207 }
208
209 /**
210 * <p>
211 * invoke.
212 * </p>
213 *
214 * @param settings This tells AWK what to do
215 * (where to get input from, where to write it to, in what mode to run,
216 * ...)
217 * @throws java.io.IOException upon an IO error.
218 * @throws java.lang.ClassNotFoundException if intermediate code is specified
219 * but deserialization fails to load in the JVM
220 * @throws org.metricshub.jawk.ExitException if interpretation is requested,
221 * and a specific exit code is requested.
222 */
223 public void invoke(String script, AwkSettings settings)
224 throws IOException,
225 ClassNotFoundException,
226 ExitException {
227 invoke(
228 new ScriptSource(
229 ScriptSource.DESCRIPTION_COMMAND_LINE_SCRIPT,
230 new StringReader(script)),
231 settings);
232 }
233
234 /**
235 * Compiles and invokes a single {@link ScriptSource} using the provided
236 * {@link AwkSettings}. This is a convenience overload for callers who have
237 * a single script to execute.
238 *
239 * @param script script source to compile and run
240 * @param settings runtime settings such as input and output streams
241 * @throws IOException if an I/O error occurs during compilation or
242 * execution
243 * @throws ClassNotFoundException if intermediate code cannot be loaded
244 * @throws ExitException if the script terminates with a non-zero exit
245 * code
246 */
247 public void invoke(ScriptSource script, AwkSettings settings)
248 throws IOException,
249 ClassNotFoundException,
250 ExitException {
251 // Delegate to the List-based overload for the actual work
252 invoke(Collections.singletonList(script), settings);
253 }
254
255 /**
256 * Compiles and invokes the specified list of {@link ScriptSource}s using the
257 * provided {@link AwkSettings}.
258 *
259 * @param scripts list of script sources to compile and run
260 * @param settings runtime settings such as input and output streams
261 * @throws IOException if an I/O error occurs during compilation or
262 * execution
263 * @throws ClassNotFoundException if intermediate code cannot be loaded
264 * @throws ExitException if the script terminates with a non-zero exit
265 * code
266 */
267 public void invoke(List<ScriptSource> scripts, AwkSettings settings)
268 throws IOException,
269 ClassNotFoundException,
270 ExitException {
271 // Compile the scripts into tuples then execute them
272 AwkTuples tuples = compile(scripts);
273 invoke(tuples, settings);
274 }
275
276 /**
277 * Interprets the specified precompiled {@link AwkTuples} using the provided
278 * {@link AwkSettings}.
279 *
280 * @param tuples precompiled tuples to interpret
281 * @param settings runtime settings
282 * @throws IOException upon an IO error
283 * @throws ExitException if interpretation is requested, and a specific exit
284 * code is requested
285 */
286 public void invoke(AwkTuples tuples, AwkSettings settings)
287 throws IOException,
288 ExitException {
289 if (tuples == null) {
290 return;
291 }
292
293 AVM avm = null;
294 try {
295 // interpret!
296 avm = createAvm(settings);
297 avm.interpret(tuples);
298 } finally {
299 if (avm != null) {
300 avm.waitForIO();
301 }
302 }
303 }
304
305 /**
306 * Executes the specified AWK script against the given input and returns the
307 * printed output as a {@link String}.
308 *
309 * @param script AWK script to execute
310 * @param input text to process
311 * @return result of the execution as a String
312 * @throws IOException if an I/O error occurs
313 * @throws ClassNotFoundException if intermediate code cannot be loaded
314 * @throws ExitException if the script terminates with a non-zero exit code
315 */
316 public String run(String script, String input)
317 throws IOException,
318 ClassNotFoundException,
319 ExitException {
320 ByteArrayOutputStream out = new ByteArrayOutputStream();
321 run(script, input, out);
322 return out.toString(StandardCharsets.UTF_8.name());
323 }
324
325 /**
326 * Executes the specified AWK script against the given input and writes the
327 * result to the provided {@link OutputStream}.
328 *
329 * @param script AWK script to execute
330 * @param input text to process
331 * @param output destination for the printed output
332 * @throws IOException if an I/O error occurs
333 * @throws ClassNotFoundException if intermediate code cannot be loaded
334 * @throws ExitException if the script terminates with a non-zero exit code
335 */
336 public void run(String script, String input, OutputStream output)
337 throws IOException,
338 ClassNotFoundException,
339 ExitException {
340 run(new StringReader(script), toInputStream(input), output, true);
341 }
342
343 /**
344 * Executes the specified AWK script against the given input and returns the
345 * printed output as a {@link String}.
346 *
347 * @param script AWK script to execute (as a {@link Reader})
348 * @param input text to process
349 * @return result of the execution as a String
350 * @throws IOException if an I/O error occurs
351 * @throws ClassNotFoundException if intermediate code cannot be loaded
352 * @throws ExitException if the script terminates with a non-zero exit code
353 */
354 public String run(Reader script, String input)
355 throws IOException,
356 ClassNotFoundException,
357 ExitException {
358 ByteArrayOutputStream out = new ByteArrayOutputStream();
359 run(script, input, out);
360 return out.toString(StandardCharsets.UTF_8.name());
361 }
362
363 /**
364 * Executes the specified AWK script against the given input and writes the
365 * result to the provided {@link OutputStream}.
366 *
367 * @param script AWK script to execute (as a {@link Reader})
368 * @param input text to process
369 * @param output destination for the printed output
370 * @throws IOException if an I/O error occurs
371 * @throws ClassNotFoundException if intermediate code cannot be loaded
372 * @throws ExitException if the script terminates with a non-zero exit code
373 */
374 public void run(Reader script, String input, OutputStream output)
375 throws IOException,
376 ClassNotFoundException,
377 ExitException {
378 run(script, toInputStream(input), output, true);
379 }
380
381 /**
382 * Executes the specified AWK script against the given input and returns the
383 * printed output as a {@link String}.
384 *
385 * @param script AWK script to execute
386 * @param input text reader to process
387 * @return result of the execution as a String
388 * @throws IOException if an I/O error occurs
389 * @throws ClassNotFoundException if intermediate code cannot be loaded
390 * @throws ExitException if the script terminates with a non-zero exit code
391 */
392 public String run(String script, Reader input)
393 throws IOException,
394 ClassNotFoundException,
395 ExitException {
396 ByteArrayOutputStream out = new ByteArrayOutputStream();
397 run(script, input, out);
398 return out.toString(StandardCharsets.UTF_8.name());
399 }
400
401 /**
402 * Executes the specified AWK script against the given input and writes the
403 * result to the provided {@link OutputStream}.
404 *
405 * @param script AWK script to execute
406 * @param input text reader to process
407 * @param output destination for the printed output
408 * @throws IOException if an I/O error occurs
409 * @throws ClassNotFoundException if intermediate code cannot be loaded
410 * @throws ExitException if the script terminates with a non-zero exit code
411 */
412 public void run(String script, Reader input, OutputStream output)
413 throws IOException,
414 ClassNotFoundException,
415 ExitException {
416 run(new StringReader(script), toInputStream(input), output, true);
417 }
418
419 /**
420 * Executes the specified AWK script against the given input and returns the
421 * printed output as a {@link String}.
422 *
423 * @param script AWK script to execute (as a {@link Reader})
424 * @param input text reader to process
425 * @return result of the execution as a String
426 * @throws IOException if an I/O error occurs
427 * @throws ClassNotFoundException if intermediate code cannot be loaded
428 * @throws ExitException if the script terminates with a non-zero exit code
429 */
430 public String run(Reader script, Reader input)
431 throws IOException,
432 ClassNotFoundException,
433 ExitException {
434 ByteArrayOutputStream out = new ByteArrayOutputStream();
435 run(script, input, out);
436 return out.toString(StandardCharsets.UTF_8.name());
437 }
438
439 /**
440 * Executes the specified AWK script against the given input and writes the
441 * result to the provided {@link OutputStream}.
442 *
443 * @param script AWK script to execute (as a {@link Reader})
444 * @param input text reader to process
445 * @param output destination for the printed output
446 * @throws IOException if an I/O error occurs
447 * @throws ClassNotFoundException if intermediate code cannot be loaded
448 * @throws ExitException if the script terminates with a non-zero exit code
449 */
450 public void run(Reader script, Reader input, OutputStream output)
451 throws IOException,
452 ClassNotFoundException,
453 ExitException {
454 run(script, toInputStream(input), output, true);
455 }
456
457 /**
458 * Executes the specified AWK script against the given input file and returns
459 * the printed output as a {@link String}.
460 *
461 * @param script AWK script to execute
462 * @param input file containing text to process
463 * @return result of the execution as a String
464 * @throws IOException if an I/O error occurs
465 * @throws ClassNotFoundException if intermediate code cannot be loaded
466 * @throws ExitException if the script terminates with a non-zero exit code
467 */
468 public String run(String script, File input)
469 throws IOException,
470 ClassNotFoundException,
471 ExitException {
472 try (InputStream in = new FileInputStream(input)) {
473 return run(script, in);
474 }
475 }
476
477 /**
478 * Executes the specified AWK script against the given input file and writes
479 * the printed output to the provided {@link OutputStream}.
480 *
481 * @param script AWK script to execute
482 * @param input file containing text to process
483 * @param output destination for the printed output
484 * @throws IOException if an I/O error occurs
485 * @throws ClassNotFoundException if intermediate code cannot be loaded
486 * @throws ExitException if the script terminates with a non-zero exit code
487 */
488 public void run(String script, File input, OutputStream output)
489 throws IOException,
490 ClassNotFoundException,
491 ExitException {
492 try (InputStream in = new FileInputStream(input)) {
493 run(script, in, output);
494 }
495 }
496
497 /**
498 * Executes the specified AWK script against the given input file and returns
499 * the printed output as a {@link String}.
500 *
501 * @param script AWK script to execute (as a {@link Reader})
502 * @param input file containing text to process
503 * @return result of the execution as a String
504 * @throws IOException if an I/O error occurs
505 * @throws ClassNotFoundException if intermediate code cannot be loaded
506 * @throws ExitException if the script terminates with a non-zero exit code
507 */
508 public String run(Reader script, File input)
509 throws IOException,
510 ClassNotFoundException,
511 ExitException {
512 try (InputStream in = new FileInputStream(input)) {
513 return run(script, in);
514 }
515 }
516
517 /**
518 * Executes the specified AWK script against the given input file and writes
519 * the printed output to the provided {@link OutputStream}.
520 *
521 * @param script AWK script to execute (as a {@link Reader})
522 * @param input file containing text to process
523 * @param output destination for the printed output
524 * @throws IOException if an I/O error occurs
525 * @throws ClassNotFoundException if intermediate code cannot be loaded
526 * @throws ExitException if the script terminates with a non-zero exit code
527 */
528 public void run(Reader script, File input, OutputStream output)
529 throws IOException,
530 ClassNotFoundException,
531 ExitException {
532 try (InputStream in = new FileInputStream(input)) {
533 run(script, in, output);
534 }
535 }
536
537 /**
538 * Executes the specified AWK script against the provided input stream and
539 * returns the printed output as a {@link String}.
540 *
541 * @param script AWK script to execute
542 * @param input stream to process
543 * @return result of the execution as a String
544 * @throws IOException if an I/O error occurs
545 * @throws ClassNotFoundException if intermediate code cannot be loaded
546 * @throws ExitException if the script terminates with a non-zero exit code
547 */
548 public String run(String script, InputStream input)
549 throws IOException,
550 ClassNotFoundException,
551 ExitException {
552 ByteArrayOutputStream out = new ByteArrayOutputStream();
553 run(script, input, out);
554 return out.toString(StandardCharsets.UTF_8.name());
555 }
556
557 /**
558 * Executes the specified AWK script against the provided input stream and
559 * writes the result to the given {@link OutputStream}.
560 *
561 * @param script AWK script to execute
562 * @param input stream to process
563 * @param output destination for the printed output
564 * @throws IOException if an I/O error occurs
565 * @throws ClassNotFoundException if intermediate code cannot be loaded
566 * @throws ExitException if the script terminates with a non-zero exit code
567 */
568 public void run(String script, InputStream input, OutputStream output)
569 throws IOException,
570 ClassNotFoundException,
571 ExitException {
572 run(new StringReader(script), input, output, false);
573 }
574
575 /**
576 * Executes the specified AWK script against the provided input stream and
577 * returns the printed output as a {@link String}.
578 *
579 * @param script AWK script to execute (as a {@link Reader})
580 * @param input stream to process
581 * @return result of the execution as a String
582 * @throws IOException if an I/O error occurs
583 * @throws ClassNotFoundException if intermediate code cannot be loaded
584 * @throws ExitException if the script terminates with a non-zero exit code
585 */
586 public String run(Reader script, InputStream input)
587 throws IOException,
588 ClassNotFoundException,
589 ExitException {
590 ByteArrayOutputStream out = new ByteArrayOutputStream();
591 run(script, input, out);
592 return out.toString(StandardCharsets.UTF_8.name());
593 }
594
595 /**
596 * Executes the specified AWK script against the provided input stream and
597 * writes the result to the given {@link OutputStream}.
598 *
599 * @param script AWK script to execute (as a {@link Reader})
600 * @param input stream to process
601 * @param output destination for the printed output
602 * @throws IOException if an I/O error occurs
603 * @throws ClassNotFoundException if intermediate code cannot be loaded
604 * @throws ExitException if the script terminates with a non-zero exit code
605 */
606 public void run(Reader script, InputStream input, OutputStream output)
607 throws IOException,
608 ClassNotFoundException,
609 ExitException {
610 run(script, input, output, false);
611 }
612
613 /**
614 * Internal method that configures default {@link AwkSettings} and executes
615 * the AWK script.
616 */
617 private void run(
618 Reader scriptReader,
619 InputStream inputStream,
620 OutputStream outputStream,
621 boolean textInput)
622 throws IOException,
623 ClassNotFoundException,
624 ExitException {
625
626 AwkSettings settings = new AwkSettings();
627 if (inputStream != null) {
628 settings.setInput(inputStream);
629 }
630
631 if (textInput) {
632 settings.setDefaultRS("\n");
633 settings.setDefaultORS("\n");
634 }
635
636 settings
637 .setOutputStream(
638 new PrintStream(
639 outputStream,
640 false,
641 StandardCharsets.UTF_8.name()));
642
643 ScriptSource script = new ScriptSource(
644 ScriptSource.DESCRIPTION_COMMAND_LINE_SCRIPT,
645 scriptReader);
646
647 try {
648 invoke(script, settings);
649 } catch (ExitException e) {
650 if (e.getCode() != 0) {
651 throw e;
652 }
653 }
654 }
655
656 /**
657 * Compiles the specified AWK script and returns the intermediate representation
658 * as {@link AwkTuples}.
659 *
660 * @param script AWK script to compile
661 * @return compiled {@link AwkTuples}
662 * @throws IOException if an I/O error occurs during compilation
663 */
664 public AwkTuples compile(String script) throws IOException {
665 ScriptSource source = new ScriptSource(
666 ScriptSource.DESCRIPTION_COMMAND_LINE_SCRIPT,
667 new StringReader(script));
668 return compile(Collections.singletonList(source));
669 }
670
671 /**
672 * Compiles the specified AWK script and returns the intermediate representation
673 * as {@link AwkTuples}.
674 *
675 * @param script AWK script to compile (as a {@link Reader})
676 * @return compiled {@link AwkTuples}
677 * @throws IOException if an I/O error occurs during compilation
678 */
679 public AwkTuples compile(Reader script) throws IOException {
680 ScriptSource source = new ScriptSource(
681 ScriptSource.DESCRIPTION_COMMAND_LINE_SCRIPT,
682 script);
683 return compile(Collections.singletonList(source));
684 }
685
686 /**
687 * Compiles a list of script sources into {@link AwkTuples} that can be
688 * interpreted by the {@link AVM} runtime.
689 *
690 * @param scripts script sources to compile
691 * @return compiled {@link AwkTuples}
692 * @throws IOException if an I/O error occurs while reading the
693 * scripts
694 */
695 public AwkTuples compile(List<ScriptSource> scripts)
696 throws IOException {
697
698 lastAst = null;
699 AwkTuples tuples = createTuples();
700 if (!scripts.isEmpty()) {
701 // Parse all script sources into a single AST
702 AwkParser parser = new AwkParser(this.extensionFunctions);
703 AstNode ast = parser.parse(scripts);
704 lastAst = ast;
705 if (ast != null) {
706 // Perform semantic checks twice to resolve forward references
707 ast.semanticAnalysis();
708 ast.semanticAnalysis();
709 // Build tuples from the AST
710 int result = ast.populateTuples(tuples);
711 assert result == 0;
712 // Assign addresses and prepare tuples for interpretation
713 tuples.postProcess();
714 // Record global variable offset mappings for the interpreter
715 parser.populateGlobalVariableNameToOffsetMappings(tuples);
716 }
717 }
718
719 return tuples;
720 }
721
722 /**
723 * Compile an expression to evaluate (not a full script).
724 *
725 * @param expression AWK expression to compile to AwkTuples
726 * @return AwkTuples to be interpreted by AVM
727 * @throws IOException if anything goes wrong with the compilation
728 */
729 public AwkTuples compileForEval(String expression) throws IOException {
730
731 // Create a ScriptSource
732 ScriptSource expressionSource = new ScriptSource(
733 ScriptSource.DESCRIPTION_COMMAND_LINE_SCRIPT,
734 new StringReader(expression));
735
736 // Parse the expression
737 AwkParser parser = new AwkParser(this.extensionFunctions);
738 AstNode ast = parser.parseExpression(expressionSource);
739
740 // Create the tuples that we will return
741 AwkTuples tuples = createTuples();
742
743 // Attempt to traverse the syntax tree and build
744 // the intermediate code
745 if (ast != null) {
746 // 1st pass to tie actual parameters to back-referenced formal parameters
747 ast.semanticAnalysis();
748 // 2nd pass to tie actual parameters to forward-referenced formal parameters
749 ast.semanticAnalysis();
750 // build tuples
751 ast.populateTuples(tuples);
752 // Calls touch(...) per Tuple so that addresses can be normalized/assigned/allocated
753 tuples.postProcess();
754 // record global_var -> offset mapping into the tuples
755 // so that the interpreter can assign variables
756 parser.populateGlobalVariableNameToOffsetMappings(tuples);
757 }
758
759 return tuples;
760 }
761
762 /**
763 * Evaluates the specified AWK expression (not a full script, just an expression)
764 * and returns the value of this expression.
765 *
766 * @param expression Expression to evaluate (e.g. <code>2+3</code>)
767 * @return the value of the specified expression
768 * @throws IOException if anything goes wrong with the evaluation
769 */
770 public Object eval(String expression) throws IOException {
771 return eval(expression, null, null);
772 }
773
774 /**
775 * Evaluates the specified AWK expression (not a full script, just an expression)
776 * and returns the value of this expression.
777 *
778 * @param expression Expression to evaluate (e.g. <code>2+3</code> or <code>$2 "-" $3</code>
779 * @param input Optional text input (that will be available as $0, and tokenized as $1, $2, etc.)
780 * @return the value of the specified expression
781 * @throws IOException if anything goes wrong with the evaluation
782 */
783 public Object eval(String expression, String input) throws IOException {
784 return eval(expression, input, null);
785 }
786
787 /**
788 * Evaluates the specified AWK expression (not a full script, just an expression)
789 * and returns the value of this expression.
790 *
791 * @param expression Expression to evaluate (e.g. <code>2+3</code> or <code>$2 "-" $3</code>
792 * @param input Optional text input (that will be available as $0, and tokenized as $1, $2, etc.)
793 * @param fieldSeparator Value of the FS global variable used for parsing the input
794 * @return the value of the specified expression
795 * @throws IOException if anything goes wrong with the evaluation
796 */
797 public Object eval(String expression, String input, String fieldSeparator) throws IOException {
798 return eval(compileForEval(expression), input, fieldSeparator);
799 }
800
801 /**
802 * Evaluates the specified AWK tuples, i.e. the result of the execution of the
803 * TERNARY_EXPRESSION AST (the value that has been pushed in the stack).
804 *
805 * @param tuples Tuples returned by {@link Awk#compileForEval(String)}
806 * @param input Optional text input (that will be available as $0, and tokenized as $1, $2, etc.)
807 * @param fieldSeparator Value of the FS global variable used for parsing the input
808 * @return the value of the specified expression
809 * @throws IOException if anything goes wrong with the evaluation
810 */
811 public Object eval(AwkTuples tuples, String input, String fieldSeparator) throws IOException {
812
813 AwkSettings settings = new AwkSettings();
814 if (input != null) {
815 settings.setInput(toInputStream(input));
816 } else {
817 settings.setInput(toInputStream(""));
818 }
819
820 settings.setDefaultRS("\n");
821 settings.setDefaultORS("\n");
822 settings.setFieldSeparator(fieldSeparator);
823
824 settings
825 .setOutputStream(
826 new PrintStream(new ByteArrayOutputStream(), false, StandardCharsets.UTF_8.name()));
827
828 AVM avm = createAvm(settings);
829 return avm.eval(tuples, input);
830 }
831
832 protected AwkTuples createTuples() {
833 return new AwkTuples();
834 }
835
836 protected AVM createAvm(AwkSettings settings) {
837 return new AVM(settings, this.extensionInstances, this.extensionFunctions);
838 }
839
840 /**
841 * Converts a text input into an {@link InputStream} using UTF-8 encoding.
842 */
843 private static InputStream toInputStream(String input) {
844 if (input == null) {
845 return new ByteArrayInputStream(new byte[0]);
846 }
847 return new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8));
848 }
849
850 /**
851 * Reads all characters from the supplied {@link Reader} and returns an
852 * {@link InputStream} containing the same data using UTF-8 encoding.
853 */
854 private static InputStream toInputStream(Reader reader) throws IOException {
855 if (reader == null) {
856 return new ByteArrayInputStream(new byte[0]);
857 }
858 StringBuilder sb = new StringBuilder();
859 char[] buf = new char[4096];
860 int len;
861 while ((len = reader.read(buf)) != -1) {
862 sb.append(buf, 0, len);
863 }
864 return new ByteArrayInputStream(sb.toString().getBytes(StandardCharsets.UTF_8));
865 }
866
867 /**
868 * Lists metadata for the {@link JawkExtension} implementations discovered on
869 * the class path.
870 *
871 * @return list of discovered extension descriptors
872 */
873 public static Map<String, JawkExtension> listAvailableExtensions() {
874 return ExtensionRegistry.listExtensions();
875 }
876
877 }