View Javadoc
1   package io.jawk.jrt;
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.FileOutputStream;
26  import java.io.FileInputStream;
27  import java.io.IOException;
28  import java.io.InputStreamReader;
29  import java.io.PrintStream;
30  import java.nio.charset.StandardCharsets;
31  import java.text.DecimalFormatSymbols;
32  import java.util.ArrayList;
33  import java.util.Date;
34  import java.util.Enumeration;
35  import java.util.HashMap;
36  import java.util.IdentityHashMap;
37  import java.util.HashSet;
38  import java.util.IllegalFormatException;
39  import java.util.List;
40  import java.util.Locale;
41  import java.util.Map;
42  import java.util.Objects;
43  import java.util.Set;
44  import java.util.StringTokenizer;
45  import java.util.regex.Matcher;
46  import java.util.regex.Pattern;
47  import io.jawk.Awk;
48  import io.jawk.intermediate.UninitializedObject;
49  import io.jawk.intermediate.UntypedObject;
50  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
51  
52  /**
53   * The Jawk runtime coordinator.
54   * The JRT services interpreted and compiled Jawk scripts, mainly
55   * for IO and other non-CPU bound tasks. The goal is to house
56   * service functions into a Java-compiled class rather than
57   * to hand-craft service functions in byte-code, or cut-paste
58   * compiled JVM code into the compiled AWK script. Also,
59   * since these functions are non-CPU bound, the need for
60   * inlining is reduced.
61   * <p>
62   * Variable access is achieved through the VariableManager interface.
63   * The constructor requires a VariableManager instance (which, in
64   * this case, is the compiled Jawk class itself).
65   * <p>
66   * Main services include:
67   * <ul>
68   * <li>File and command output redirection via print(f).
69   * <li>File and command input redirection via getline.
70   * <li>Most built-in AWK functions, such as system(), sprintf(), etc.
71   * <li>Automatic AWK type conversion routines.
72   * <li>IO management for input rule processing.
73   * <li>Random number engine management.
74   * <li>Input field ($0, $1, ...) management.
75   * </ul>
76   * <p>
77   * All static and non-static service methods should be package-private
78   * to the resultant AWK script class rather than public. However,
79   * the resultant script class is not in the <code>io.jawk.jrt</code> package
80   * by default, and the user may reassign the resultant script class
81   * to another package. Therefore, all accessed methods are public.
82   *
83   * @see VariableManager
84   * @author Danny Daglas
85   */
86  public class JRT {
87  
88  	private static final boolean IS_WINDOWS = System.getProperty("os.name").indexOf("Windows") >= 0;
89  
90  	/**
91  	 * Ceiling for {@link #dynamicPatterns} and {@link #dynamicPatternsIgnoreCase}:
92  	 * scripts can synthesize an unbounded number of distinct dynamic regexps from
93  	 * input data, so a cache is dumped wholesale when it fills instead of growing
94  	 * without bound. Real scripts use a handful of dynamic regexps, so the limit
95  	 * is effectively never reached.
96  	 */
97  	private static final int DYNAMIC_PATTERN_CACHE_LIMIT = 256;
98  
99  	private final VariableManager vm;
100 
101 	private IoState ioState;
102 	/** Output sink used for plain AWK print/printf output. */
103 	private AwkSink awkSink;
104 	/** PrintStream used for command error output */
105 	private PrintStream error;
106 	/** PrintStream used for runtime warning messages, stderr by default. */
107 	private PrintStream warning = System.err;
108 	/** Current IGNORECASE value, as assigned by the script or the host. */
109 	private Object ignorecase = Long.valueOf(0L);
110 	/** Precomputed truth of IGNORECASE, consulted by every regexp operation. */
111 	private boolean ignoreCase;
112 	/** Case-insensitive twins of precompiled patterns; created on first use. */
113 	private Map<Pattern, Pattern> caseInsensitivePatterns;
114 	/** Compiled case-sensitive dynamic (string) regexps, keyed by expression text; created on first use. */
115 	private Map<String, Pattern> dynamicPatterns;
116 	/** Compiled case-insensitive dynamic (string) regexps, keyed by expression text; created on first use. */
117 	private Map<String, Pattern> dynamicPatternsIgnoreCase;
118 	/** Reused buffer holding the result of the last sub()/gsub() replacement. */
119 	private final StringBuffer replaceResult = new StringBuffer();
120 	// Last input line consumed for getline-style transport.
121 	private Object inputLine = null;
122 	// Current record state ($0, $1, $2, ...).
123 	private RecordState recordState;
124 	// The currently active InputSource (set during consumeInput calls).
125 	private InputSource activeSource;
126 	private static final UninitializedObject BLANK = new UninitializedObject();
127 
128 	private static final Integer ONE = Integer.valueOf(1);
129 	private static final Integer ZERO = Integer.valueOf(0);
130 	private static final Integer MINUS_ONE = Integer.valueOf(-1);
131 	private String jrtInputString;
132 
133 	// JRT-managed special variables (runtime only)
134 	private long nr; // total record number
135 	private long fnr; // file record number
136 	private int rstart; // last match start (1-based)
137 	private int rlength; // last match length
138 	private Object filename; // current input filename scalar (or empty for stdin/pipe)
139 	private Object errno; // last input I/O error description (gawk ERRNO)
140 	private Object argind; // ARGV index of the current input file (gawk ARGIND)
141 	private boolean syntheticFilePresented; // custom InputSource already presented as a single "file"
142 	private String fs; // field separator
143 	private String rs; // record separator (regexp)
144 	private String ofs; // output field separator
145 	private String ors; // output record separator
146 	private String convfmt; // number-to-string format
147 	private String ofmt; // number-to-string for output
148 	private String subsep; // subscript separator
149 	private final Locale locale; // locale for number formatting
150 	private final char decimalSeparator; // locale decimal separator for strnum recognition
151 
152 	private static final class FileOutputState {
153 
154 		private final AwkSink sink;
155 
156 		private FileOutputState(AwkSink sinkParam) {
157 			this.sink = Objects.requireNonNull(sinkParam, "sink");
158 		}
159 	}
160 
161 	private static final class CommandInputState {
162 
163 		private final Process process;
164 		private final PartitioningReader reader;
165 		private final Thread errorPump;
166 
167 		private CommandInputState(Process processParam, PartitioningReader readerParam, Thread errorPumpParam) {
168 			this.process = Objects.requireNonNull(processParam, "process");
169 			this.reader = Objects.requireNonNull(readerParam, "reader");
170 			this.errorPump = errorPumpParam;
171 		}
172 	}
173 
174 	private static final class ProcessOutputState {
175 
176 		private final Process process;
177 		private final AwkSink sink;
178 		private final PrintStream processOutput;
179 		private final Thread stdoutPump;
180 		private final Thread stderrPump;
181 
182 		private ProcessOutputState(
183 				Process processParam,
184 				AwkSink sinkParam,
185 				PrintStream processOutputParam,
186 				Thread stdoutPumpParam,
187 				Thread stderrPumpParam) {
188 			this.process = Objects.requireNonNull(processParam, "process");
189 			this.sink = Objects.requireNonNull(sinkParam, "sink");
190 			this.processOutput = Objects.requireNonNull(processOutputParam, "processOutput");
191 			this.stdoutPump = stdoutPumpParam;
192 			this.stderrPump = stderrPumpParam;
193 		}
194 	}
195 
196 	private static final class IoState {
197 
198 		private final Map<String, PartitioningReader> fileReaders = new HashMap<String, PartitioningReader>();
199 		private final Map<String, CommandInputState> commandInputs = new HashMap<String, CommandInputState>();
200 		private final Map<String, FileOutputState> fileOutputs = new HashMap<String, FileOutputState>();
201 		private final Map<String, ProcessOutputState> processOutputs = new HashMap<String, ProcessOutputState>();
202 	}
203 
204 	/**
205 	 * Create a JRT with explicit default output and error streams.
206 	 *
207 	 * @param vm The VariableManager to use with this JRT.
208 	 * @param locale The Locale to use for number formatting.
209 	 * @param awkSink default output sink used by plain AWK print operations
210 	 * @param error default error stream used for process stderr
211 	 */
212 	@SuppressFBWarnings(value = {
213 			"EI_EXPOSE_REP2",
214 			"CT_CONSTRUCTOR_THROW" }, justification = "JRT must hold the provided runtime collaborators for later use;"
215 					+ " fail-fast argument validation with no security-sensitive state to protect from finalizer attacks")
216 	public JRT(VariableManager vm, Locale locale, AwkSink awkSink, PrintStream error) {
217 		this.vm = vm;
218 		this.locale = locale == null ? Locale.US : locale;
219 		this.decimalSeparator = DecimalFormatSymbols.getInstance(this.locale).getDecimalSeparator();
220 		this.awkSink = Objects.requireNonNull(awkSink, "awkSink");
221 		this.error = error == null ? System.err : error;
222 		this.nr = 0L;
223 		this.fnr = 0L;
224 		this.rstart = 0;
225 		this.rlength = 0;
226 		this.filename = "";
227 		this.fs = Awk.DEFAULT_FS;
228 		this.rs = Awk.DEFAULT_RS;
229 		this.ofs = Awk.DEFAULT_OFS;
230 		this.ors = Awk.DEFAULT_ORS;
231 		this.convfmt = Awk.DEFAULT_CONVFMT;
232 		this.ofmt = Awk.DEFAULT_OFMT;
233 		this.subsep = Awk.DEFAULT_SUBSEP;
234 	}
235 
236 	/**
237 	 * Sets the sink used by default {@code print} and {@code printf}
238 	 * operations.
239 	 *
240 	 * @param sink output sink to use
241 	 */
242 	public void setAwkSink(AwkSink sink) {
243 		awkSink = Objects.requireNonNull(sink, "awkSink");
244 	}
245 
246 	/**
247 	 * Sets the stream used for the stderr output of spawned processes
248 	 * (e.g.&nbsp;{@code system("...")}).
249 	 *
250 	 * @param errorStream stream to receive process stderr
251 	 */
252 	public void setErrorStream(PrintStream errorStream) {
253 		this.error = Objects.requireNonNull(errorStream, "errorStream");
254 	}
255 
256 	/**
257 	 * Sets the stream that receives runtime warning messages. Warnings default
258 	 * to {@link System#err}, mirroring where gawk sends its diagnostics, and are
259 	 * deliberately kept apart from the process-stderr stream so they can never
260 	 * leak into a captured script output.
261 	 *
262 	 * @param warningStream stream to receive runtime warnings
263 	 */
264 	public void setWarningStream(PrintStream warningStream) {
265 		this.warning = Objects.requireNonNull(warningStream, "warningStream");
266 	}
267 
268 	/**
269 	 * Prints a runtime warning message to the warning stream (stderr by
270 	 * default), mirroring where gawk sends its diagnostics.
271 	 *
272 	 * @param message warning text to print
273 	 */
274 	public void printWarning(String message) {
275 		warning.println(message);
276 		warning.flush();
277 	}
278 
279 	/**
280 	 * Returns the default output sink used by {@code print} and {@code printf}.
281 	 *
282 	 * @return the current AWK sink
283 	 */
284 	public AwkSink getAwkSink() {
285 		return awkSink;
286 	}
287 
288 	/**
289 	 * Returns the locale used for number formatting in this runtime.
290 	 *
291 	 * @return the runtime locale
292 	 */
293 	public Locale getLocale() {
294 		return locale;
295 	}
296 
297 	private IoState getIoState() {
298 		if (ioState == null) {
299 			ioState = new IoState();
300 		}
301 		return ioState;
302 	}
303 
304 	/**
305 	 * Returns whether the supplied variable name is managed directly by JRT
306 	 * rather than through the AVM runtime stack.
307 	 *
308 	 * @param name variable name to inspect
309 	 * @return {@code true} when the variable is a JRT-managed special variable
310 	 */
311 	public static boolean isJrtManagedSpecialVariable(String name) {
312 		switch (name) {
313 		case "FS":
314 		case "RS":
315 		case "OFS":
316 		case "ORS":
317 		case "CONVFMT":
318 		case "OFMT":
319 		case "SUBSEP":
320 		case "FILENAME":
321 		case "NF":
322 		case "NR":
323 		case "FNR":
324 		case "ARGC":
325 		case "IGNORECASE":
326 		case "ERRNO":
327 		case "ARGIND":
328 			return true;
329 		default:
330 			return false;
331 		}
332 	}
333 
334 	/**
335 	 * Returns whether the name is a gawk-only special variable that POSIX
336 	 * mode treats as an ordinary identifier, like {@code gawk --posix} does.
337 	 * Shared by the parser and the interpreter so both stay in sync.
338 	 *
339 	 * @param name variable name to inspect
340 	 * @return {@code true} when POSIX mode must treat the name as ordinary
341 	 */
342 	public static boolean isGawkOnlySpecialVariable(String name) {
343 		return "ERRNO".equals(name) || "ARGIND".equals(name);
344 	}
345 
346 	/**
347 	 * Copies only the JRT-managed special variables from the supplied map.
348 	 *
349 	 * @param variableMap source variable map
350 	 * @return a new map containing only JRT-managed special variables
351 	 */
352 	public static Map<String, Object> copySpecialVariables(Map<String, Object> variableMap) {
353 		Map<String, Object> specialVariables = new HashMap<String, Object>();
354 		if (variableMap == null || variableMap.isEmpty()) {
355 			return specialVariables;
356 		}
357 		for (Map.Entry<String, Object> entry : variableMap.entrySet()) {
358 			if (isJrtManagedSpecialVariable(entry.getKey())) {
359 				specialVariables.put(entry.getKey(), entry.getValue());
360 			}
361 		}
362 		return specialVariables;
363 	}
364 
365 	/**
366 	 * Resets per-execution JRT state and re-applies the default runtime special
367 	 * variables for a new script or expression execution.
368 	 * <p>
369 	 * The {@code defaultFs} and {@code defaultRs} parameters allow the caller
370 	 * to configure the initial field and record separators. Other special variables
371 	 * ({@code OFS}, {@code ORS}, {@code CONVFMT}, {@code OFMT}, {@code SUBSEP})
372 	 * use their POSIX-mandated defaults (see {@link Awk} constants) which are
373 	 * platform-independent and therefore not parameterized. Platform-specific
374 	 * end-of-line handling is the responsibility of the {@link AwkSink}.
375 	 *
376 	 * @param defaultFs default field separator, or {@code null} for
377 	 *        {@link Awk#DEFAULT_FS}
378 	 * @param defaultRs default record separator
379 	 */
380 	public void prepareForExecution(String defaultFs, String defaultRs) {
381 		// Close any previously opened IO resources before resetting state.
382 		jrtCloseAll();
383 
384 		// Clear per-execution state (IO handles, counters, input state).
385 		ioState = null;
386 		inputLine = null;
387 		recordState = null;
388 		activeSource = null;
389 		jrtInputString = null;
390 		nr = 0L;
391 		fnr = 0L;
392 		rstart = 0;
393 		rlength = 0;
394 		filename = "";
395 		errno = "";
396 		argind = ZERO;
397 		syntheticFilePresented = false;
398 
399 		// Apply default runtime special variables.
400 		setFS(defaultFs == null ? Awk.DEFAULT_FS : defaultFs);
401 		setRS(defaultRs);
402 		setOFS(Awk.DEFAULT_OFS);
403 		setORS(Awk.DEFAULT_ORS);
404 		setCONVFMT(Awk.DEFAULT_CONVFMT);
405 		setOFMT(Awk.DEFAULT_OFMT);
406 		setSUBSEP(Awk.DEFAULT_SUBSEP);
407 		setFILENAMEViaJrt("");
408 		setNR(0);
409 		setFNR(0);
410 		setRSTART(0);
411 		setRLENGTH(0);
412 		setIGNORECASE(Long.valueOf(0L));
413 	}
414 
415 	/**
416 	 * Assign all -v variables.
417 	 *
418 	 * @param initialVarMap A map containing all initial variable
419 	 *        names and their values.
420 	 */
421 	public final void assignInitialVariables(Map<String, Object> initialVarMap) {
422 		for (Map.Entry<String, Object> var : initialVarMap.entrySet()) {
423 			String name = var.getKey();
424 			Object value = var.getValue();
425 			if (!applySpecialVariable(name, value)) {
426 				vm.assignVariable(name, value);
427 			}
428 		}
429 	}
430 
431 	/**
432 	 * Applies the assignment of a single JRT-managed special variable.
433 	 *
434 	 * @param name variable name
435 	 * @param value value to assign
436 	 * @return {@code true} when the name was a JRT-managed special variable,
437 	 *         {@code false} when the assignment was not handled
438 	 */
439 	public boolean applySpecialVariable(String name, Object value) {
440 		switch (name) {
441 		case "FS":
442 			setFS(value);
443 			return true;
444 		case "RS":
445 			setRS(value);
446 			return true;
447 		case "OFS":
448 			setOFS(value);
449 			return true;
450 		case "ORS":
451 			setORS(value);
452 			return true;
453 		case "CONVFMT":
454 			setCONVFMT(value);
455 			return true;
456 		case "OFMT":
457 			setOFMT(value);
458 			return true;
459 		case "SUBSEP":
460 			setSUBSEP(value);
461 			return true;
462 		case "FILENAME":
463 			setFILENAMEViaJrt(value);
464 			return true;
465 		case "NF":
466 			setNF(value);
467 			return true;
468 		case "NR":
469 			setNR(value);
470 			return true;
471 		case "FNR":
472 			setFNR(value);
473 			return true;
474 		case "ARGC":
475 			setARGC(value);
476 			return true;
477 		case "IGNORECASE":
478 			setIGNORECASE(value);
479 			return true;
480 		case "ERRNO":
481 			setERRNO(value);
482 			return true;
483 		case "ARGIND":
484 			setARGIND(value);
485 			return true;
486 		default:
487 			return false;
488 		}
489 	}
490 
491 	/**
492 	 * Applies only the JRT-managed special variable assignments from the
493 	 * supplied map (FS, RS, OFS, ORS, CONVFMT, OFMT, SUBSEP, FILENAME, NF,
494 	 * NR, FNR, ARGC, IGNORECASE). Non-special variables are silently skipped because
495 	 * they require the runtime stack to be fully initialized (which happens
496 	 * during tuple execution).
497 	 *
498 	 * @param variableMap a map of variable names to values
499 	 */
500 	public final void applySpecialVariables(Map<String, Object> variableMap) {
501 		if (variableMap == null || variableMap.isEmpty()) {
502 			return;
503 		}
504 		for (Map.Entry<String, Object> var : variableMap.entrySet()) {
505 			// Non-special variables are skipped; they are assigned later
506 			// via the tuple instruction stream
507 			applySpecialVariable(var.getKey(), var.getValue());
508 		}
509 	}
510 
511 	/**
512 	 * Called by AVM/compiled modules to assign local
513 	 * environment variables to an associative array
514 	 * (in this case, to ENVIRON).
515 	 *
516 	 * @param aa The associative array to populate with
517 	 *        environment variables. The module asserts that
518 	 *        the associative array is empty prior to population.
519 	 */
520 	public static void assignEnvironmentVariables(AssocArray aa) {
521 		Map<String, String> env = System.getenv();
522 		for (Map.Entry<String, String> var : env.entrySet()) {
523 			aa.put(var.getKey(), new StrNum(var.getValue()));
524 		}
525 	}
526 
527 	/**
528 	 * Creates an AWK-managed associative array and exposes it as a plain
529 	 * {@link Map} for callers that do not need the concrete runtime type.
530 	 *
531 	 * @param sortedArrayKeys {@code true} to keep keys sorted
532 	 * @return a new AWK associative array
533 	 */
534 	public static Map<Object, Object> createAwkMap(boolean sortedArrayKeys) {
535 		return AssocArray.create(sortedArrayKeys);
536 	}
537 
538 	/**
539 	 * Checks key existence using AWK semantics when the supplied map is backed by
540 	 * an {@link AssocArray}, otherwise falling back to regular {@link Map}
541 	 * semantics.
542 	 *
543 	 * @param map map to inspect
544 	 * @param key key to look up
545 	 * @return {@code true} when the key exists
546 	 */
547 	public static boolean containsAwkKey(Map<Object, Object> map, Object key) {
548 		if (map instanceof AssocArray) {
549 			return ((AssocArray) map).isIn(key);
550 		}
551 		return map.containsKey(key);
552 	}
553 
554 	/**
555 	 * Reads a map element using AWK semantics when the supplied map is backed by
556 	 * an {@link AssocArray}. For plain {@link Map} instances, missing or
557 	 * {@code null}-valued entries are exposed as the AWK blank value so later
558 	 * expression evaluation never receives a raw {@code null}.
559 	 *
560 	 * @param map map to inspect
561 	 * @param key key to look up
562 	 * @return the stored value, or the AWK blank value when no concrete value is
563 	 *         present
564 	 */
565 	public static Object getAssocArrayValue(Map<Object, Object> map, Object key) {
566 		if (map instanceof AssocArray) {
567 			return map.get(key);
568 		}
569 		Object value = map.get(key);
570 		return value != null ? value : BLANK;
571 	}
572 
573 	/**
574 	 * Returns the AWK string value of an associative array entry, or
575 	 * {@code null} when the array has no such key. This is the common way to
576 	 * read optional settings out of AWK arrays, such as
577 	 * {@code PROCINFO["sorted_in"]} or {@code ENVIRON["TZ"]}.
578 	 *
579 	 * @param map associative array to read
580 	 * @param key entry key
581 	 * @return the entry value converted with {@code CONVFMT}, or {@code null}
582 	 *         when the key is absent
583 	 */
584 	public String getAwkStringEntry(Map<Object, Object> map, Object key) {
585 		if (!containsAwkKey(map, key)) {
586 			return null;
587 		}
588 		return toAwkString(getAssocArrayValue(map, key));
589 	}
590 
591 	/**
592 	 * Convert Strings, Integers, and Doubles to Strings
593 	 * based on the CONVFMT variable contents and the stored Locale.
594 	 *
595 	 * @param o Object to convert.
596 	 * @return A String representation of o.
597 	 */
598 	public String toAwkString(Object o) {
599 		return AwkSink.formatOutputValue(o, this.convfmt, this.locale);
600 	}
601 
602 	/**
603 	 * Convert a String, Integer, or Double to Double.
604 	 *
605 	 * @param o Object to convert.
606 	 * @return the "double" value of o, or 0 if invalid
607 	 */
608 	public static double toDouble(final Object o) {
609 		if (o == null) {
610 			return 0;
611 		}
612 
613 		if (o instanceof Number) {
614 			return ((Number) o).doubleValue();
615 		}
616 
617 		if (o instanceof Character) {
618 			return (double) ((Character) o).charValue();
619 		}
620 
621 		if (o instanceof StrNum) {
622 			StrNum strNum = (StrNum) o;
623 			if (strNum.isNumber()) {
624 				return strNum.doubleValue();
625 			}
626 		}
627 
628 		// Try to convert the string to a number.
629 		String s = o.toString();
630 		int length = s.length();
631 
632 		// Optimization: We don't need to handle strings that are longer than 26 chars
633 		// because a Double cannot be longer than 26 chars when converted to String.
634 		if (length > 26) {
635 			length = 26;
636 		}
637 
638 		// Loop:
639 		// If convervsion fails, try with one character less.
640 		// 25fix will convert to 25 (any numeric prefix will work)
641 		while (length > 0) {
642 			try {
643 				return Double.parseDouble(s.substring(0, length));
644 			} catch (NumberFormatException nfe) {
645 				length--;
646 			}
647 		}
648 
649 		// Failed (not even with one char)
650 		return 0;
651 	}
652 
653 	/**
654 	 * Determines whether a double value actually represents a long integer
655 	 * within the limits of floating point precision.
656 	 *
657 	 * @param d the double value to examine
658 	 * @return {@code true} if {@code d} is effectively an integer
659 	 */
660 	public static boolean isActuallyLong(double d) {
661 		double r = Math.rint(d);
662 		return Math.abs(d - r) < Math.ulp(d);
663 	}
664 
665 	/**
666 	 * Convert a String, Long, or Double to Long.
667 	 *
668 	 * @param o Object to convert.
669 	 * @return the "long" value of o, or 0 if invalid
670 	 */
671 	public static long toLong(final Object o) {
672 		if (o == null) {
673 			return 0;
674 		}
675 
676 		if (o instanceof Number) {
677 			return ((Number) o).longValue();
678 		}
679 
680 		if (o instanceof Character) {
681 			return (long) ((Character) o).charValue();
682 		}
683 
684 		// Try to convert the string to a number.
685 		String s = o.toString();
686 		int length = s.length();
687 
688 		// Optimization: We don't need to handle strings that are longer than 20 chars
689 		// because a Long cannot be longer than 20 chars when converted to String.
690 		if (length > 20) {
691 			length = 20;
692 		}
693 
694 		// Loop:
695 		// If convervsion fails, try with one character less.
696 		// 25fix will convert to 25 (any numeric prefix will work)
697 		while (length > 0) {
698 			try {
699 				return Long.parseLong(s.substring(0, length));
700 			} catch (NumberFormatException nfe) {
701 				length--;
702 			}
703 		}
704 		// Failed (not even with one char)
705 		return 0;
706 	}
707 
708 	/**
709 	 * Convert a field designator to a non-negative long, raising an AWK runtime
710 	 * exception when the value is invalid.
711 	 *
712 	 * @param obj the object identifying the field (for example, the result of a
713 	 *        numeric expression)
714 	 * @return the parsed field number as a long
715 	 */
716 	public static long parseFieldNumber(Object obj) {
717 		long num = toLong(obj);
718 		if (num < 0) {
719 			throw new AwkRuntimeException(
720 					"Field $(" + obj.toString()
721 							+ ") is incorrect.");
722 		}
723 		return num;
724 	}
725 
726 	/**
727 	 * Compares two objects. Whether to employ less-than, equals, or
728 	 * greater-than checks depends on the mode chosen by the callee.
729 	 * It handles Awk variable rules and type conversion semantics.
730 	 *
731 	 * @param o1 The 1st object.
732 	 * @param o2 the 2nd object.
733 	 * @param mode
734 	 *        <ul>
735 	 *        <li>&lt; 0 - Return true if o1 &lt; o2.
736 	 *        <li>0 - Return true if o1 == o2.
737 	 *        <li>&gt; 0 - Return true if o1 &gt; o2.
738 	 *        </ul>
739 	 * @return a boolean
740 	 */
741 	public static boolean compare2(Object o1, Object o2, int mode) {
742 		return compare2(o1, o2, mode, false);
743 	}
744 
745 	/**
746 	 * Compares two objects like {@link #compare2(Object, Object, int)}, folding
747 	 * case in string comparisons when {@code ignoreCase} is set: gawk's
748 	 * {@code IGNORECASE} applies to string relational operators, not only to
749 	 * regexp operations.
750 	 *
751 	 * @param o1 The 1st object.
752 	 * @param o2 the 2nd object.
753 	 * @param mode the comparison mode, as in {@link #compare2(Object, Object, int)}
754 	 * @param ignoreCase whether string comparisons ignore case
755 	 * @return a boolean
756 	 */
757 	public static boolean compare2(Object o1, Object o2, int mode, boolean ignoreCase) {
758 		if (o1 instanceof Number && o2 instanceof Number) {
759 			return compareNumbers(((Number) o1).doubleValue(), ((Number) o2).doubleValue(), mode);
760 		}
761 
762 		String o1String = o1 == null ? "" : o1.toString();
763 		String o2String = o2 == null ? "" : o2.toString();
764 
765 		if (o1 instanceof UninitializedObject) {
766 			if (isBlankOrZero(o2, o2String)) {
767 				return mode == 0;
768 			} else {
769 				return mode < 0;
770 			}
771 		}
772 		if (o2 instanceof UninitializedObject) {
773 			if (isBlankOrZero(o1, o1String)) {
774 				return mode == 0;
775 			} else {
776 				return mode > 0;
777 			}
778 		}
779 
780 		if (isNumericComparisonOperand(o1) && isNumericComparisonOperand(o2)) {
781 			return compareNumbers(getDoubleForComparison(o1), getDoubleForComparison(o2), mode);
782 		}
783 
784 		if (mode == 0) {
785 			return ignoreCase ? o1String.equalsIgnoreCase(o2String) : o1String.equals(o2String);
786 		}
787 		int comparison = ignoreCase ? o1String.compareToIgnoreCase(o2String) : o1String.compareTo(o2String);
788 		return mode < 0 ? comparison < 0 : comparison > 0;
789 	}
790 
791 	/**
792 	 * Implements the {@code index()} builtin: the 1-based position of
793 	 * {@code needle} within {@code haystack}, or 0 when absent, folding case
794 	 * when {@code IGNORECASE} is set.
795 	 *
796 	 * @param haystack text to search
797 	 * @param needle text to find
798 	 * @return 1-based match position, 0 when not found
799 	 */
800 	public int index(String haystack, String needle) {
801 		if (!ignoreCase) {
802 			return haystack.indexOf(needle) + 1;
803 		}
804 		int max = haystack.length() - needle.length();
805 		for (int i = 0; i <= max; i++) {
806 			if (haystack.regionMatches(true, i, needle, 0, needle.length())) {
807 				return i + 1;
808 			}
809 		}
810 		return 0;
811 	}
812 
813 	private static boolean isBlankOrZero(Object value, String stringValue) {
814 		if (value instanceof UninitializedObject) {
815 			return true;
816 		}
817 		if (value instanceof Number) {
818 			return ((Number) value).doubleValue() == 0.0D;
819 		}
820 		if (value instanceof StrNum && ((StrNum) value).isNumber()) {
821 			return ((StrNum) value).doubleValue() == 0.0D;
822 		}
823 		return "".equals(stringValue) || "0".equals(stringValue);
824 	}
825 
826 	private static boolean isNumericComparisonOperand(Object value) {
827 		return value instanceof Number || value instanceof StrNum && ((StrNum) value).isNumber();
828 	}
829 
830 	private static double getDoubleForComparison(Object value) {
831 		if (value instanceof Number) {
832 			return ((Number) value).doubleValue();
833 		}
834 		return ((StrNum) value).doubleValue();
835 	}
836 
837 	private static boolean compareNumbers(double o1Number, double o2Number, int mode) {
838 		if (mode < 0) {
839 			return o1Number < o2Number;
840 		} else if (mode == 0) {
841 			return o1Number == o2Number;
842 		} else {
843 			return o1Number > o2Number;
844 		}
845 	}
846 
847 	/**
848 	 * Converts an internal runtime scalar to the value exposed through Java APIs.
849 	 *
850 	 * @param value internal scalar value
851 	 * @return plain Java scalar value
852 	 */
853 	public static Object toJavaScalar(Object value) {
854 		if (value instanceof StrNum) {
855 			return value.toString();
856 		}
857 		if (value instanceof Double || value instanceof Float) {
858 			double number = ((Number) value).doubleValue();
859 			if (isActuallyLong(number)) {
860 				return Long.valueOf((long) Math.rint(number));
861 			}
862 		}
863 		return value;
864 	}
865 
866 	/**
867 	 * Returns whether the supplied text parses as an AWK number under this
868 	 * runtime's locale, as used for strnum recognition.
869 	 *
870 	 * @param value text to test
871 	 * @return {@code true} when {@code value} is an input numeric string
872 	 */
873 	public boolean isParseableNumber(String value) {
874 		return isParseableNumber(value, decimalSeparator);
875 	}
876 
877 	/**
878 	 * Replaces the untyped marker by AWK's assigned blank scalar. Reading a
879 	 * missing array element creates and returns the untyped marker (so
880 	 * {@code typeof()} can see it), but an assignment must not propagate it:
881 	 * after {@code x = a[missing]}, {@code x} is an assigned blank scalar
882 	 * ({@code typeof(x) == "unassigned"}), exactly as in gawk. This is a single
883 	 * {@code instanceof} on the assignment paths.
884 	 *
885 	 * @param value value about to be stored by an assignment
886 	 * @return the assigned blank scalar when the value was the untyped marker,
887 	 *         otherwise the original value
888 	 */
889 	public static Object untypedToBlank(Object value) {
890 		return value instanceof UntypedObject ? BLANK : value;
891 	}
892 
893 	static boolean isParseableNumber(String value, char decimalSeparator) {
894 		int index = 0;
895 		int length = value.length();
896 
897 		if (length == 0) {
898 			return false;
899 		}
900 
901 		char current = value.charAt(index);
902 		if (current == '+' || current == '-') {
903 			index++;
904 			if (index == length) {
905 				return false;
906 			}
907 		}
908 
909 		boolean digitFound = false;
910 		while (index < length && value.charAt(index) >= '0' && value.charAt(index) <= '9') {
911 			index++;
912 			digitFound = true;
913 		}
914 
915 		if (index < length && value.charAt(index) == decimalSeparator) {
916 			index++;
917 			while (index < length && value.charAt(index) >= '0' && value.charAt(index) <= '9') {
918 				index++;
919 				digitFound = true;
920 			}
921 		}
922 
923 		if (!digitFound) {
924 			return false;
925 		}
926 
927 		if (index < length && (value.charAt(index) == 'e' || value.charAt(index) == 'E')) {
928 			index++;
929 			if (index < length && (value.charAt(index) == '+' || value.charAt(index) == '-')) {
930 				index++;
931 			}
932 
933 			boolean exponentDigitFound = false;
934 			while (index < length && value.charAt(index) >= '0' && value.charAt(index) <= '9') {
935 				index++;
936 				exponentDigitFound = true;
937 			}
938 			if (!exponentDigitFound) {
939 				return false;
940 			}
941 		}
942 
943 		return index == length;
944 	}
945 
946 	static String normalizeNumberForComparison(String value, char decimalSeparator) {
947 		return decimalSeparator == '.' ? value : value.replace(decimalSeparator, '.');
948 	}
949 
950 	/**
951 	 * Return an object which is numerically equivalent to
952 	 * one plus a given object. For Integers and Doubles,
953 	 * this is similar to o+1. For Strings, attempts are
954 	 * made to convert it to a double first. If the
955 	 * String does not contain a numeric prefix, 1 is returned.
956 	 *
957 	 * @param o The object to increase.
958 	 * @return {@code o + 1} if o is numeric or contains a numeric prefix;
959 	 *         otherwise, {@code 1.0}
960 	 */
961 	public static Object inc(Object o) {
962 		return toDouble(o) + 1;
963 	}
964 
965 	/**
966 	 * Return an object which is numerically equivalent to
967 	 * one minus a given object. For Integers and Doubles,
968 	 * this is similar to o-1. For Strings, attempts are
969 	 * made to convert it to a double first. If the
970 	 * String does not contain a numeric prefix, -1 is returned.
971 	 *
972 	 * @param o The object to increase.
973 	 * @return {@code o - 1} if o is numeric or contains a numeric prefix;
974 	 *         otherwise, {@code -1.0}
975 	 */
976 	public static Object dec(Object o) {
977 		return toDouble(o) - 1;
978 	}
979 
980 	// non-static to reference "inputLine"
981 	/**
982 	 * Converts an Integer, Double, String, Pattern,
983 	 * or ConditionPair to a boolean.
984 	 *
985 	 * @param o The object to convert to a boolean.
986 	 * @return For the following class types for o:
987 	 *         <ul>
988 	 *         <li><strong>Integer</strong> - o.intValue() != 0
989 	 *         <li><strong>Long</strong> - o.longValue() != 0
990 	 *         <li><strong>Double</strong> - o.doubleValue() != 0
991 	 *         <li><strong>String</strong> - o.length() &gt; 0
992 	 *         <li><strong>UninitializedObject</strong> - false
993 	 *         <li><strong>Pattern</strong> - $0 ~ o
994 	 *         </ul>
995 	 *         If o is none of these types, an error is thrown.
996 	 */
997 	public final boolean toBoolean(Object o) {
998 		boolean val;
999 		if (o instanceof Integer) {
1000 			val = ((Integer) o).intValue() != 0;
1001 		} else if (o instanceof Long) {
1002 			val = ((Long) o).longValue() != 0;
1003 		} else if (o instanceof Double) {
1004 			val = ((Double) o).doubleValue() != 0;
1005 		} else if (o instanceof StrNum) {
1006 			StrNum strNum = (StrNum) o;
1007 			val = strNum.isNumber() ? strNum.doubleValue() != 0 : strNum.toString().length() > 0;
1008 		} else if (o instanceof String) {
1009 			val = (o.toString().length() > 0);
1010 		} else if (o instanceof UninitializedObject) {
1011 			val = false;
1012 		} else if (o instanceof Pattern) {
1013 			// match against $0
1014 			Pattern pattern = caseAwarePattern((Pattern) o);
1015 			Object inputField = jrtGetInputField(0);
1016 			String s = inputField instanceof UninitializedObject ? "" : inputField.toString();
1017 			Matcher matcher = pattern.matcher(s);
1018 			val = matcher.find();
1019 		} else {
1020 			throw new Error("Unknown operand_stack type: " + o.getClass() + " for value " + o);
1021 		}
1022 		return val;
1023 	}
1024 
1025 	/**
1026 	 * Splits the string into parts separated by one or more spaces;
1027 	 * blank first and last fields are eliminated.
1028 	 * This conforms to the 2-argument version of AWK's split function.
1029 	 *
1030 	 * @param array The array to populate.
1031 	 * @param string The string to split.
1032 	 * @return The number of parts resulting from this split operation.
1033 	 */
1034 	public int split(Object array, Object string) {
1035 		return splitWorker(new StringTokenizer(toAwkString(string)), toArrayMap(array));
1036 	}
1037 
1038 	/**
1039 	 * Splits the string into parts separated the regular expression fs.
1040 	 * This conforms to the 3-argument version of AWK's split function.
1041 	 * <p>
1042 	 * If fs is blank, it behaves similar to the 2-arg version of
1043 	 * AWK's split function.
1044 	 *
1045 	 * @param fieldSeparator Field separator regular expression.
1046 	 * @param array The array to populate.
1047 	 * @param string The string to split.
1048 	 * @return The number of parts resulting from this split operation.
1049 	 */
1050 	public int split(Object fieldSeparator, Object array, Object string) {
1051 		return splitWorker(splitTokenizer(toAwkString(string), fieldSeparator), toArrayMap(array));
1052 	}
1053 
1054 	private static Map<Object, Object> toArrayMap(Object array) {
1055 		if (!(array instanceof Map)) {
1056 			throw new IllegalArgumentException("split target must be a Map.");
1057 		}
1058 		@SuppressWarnings("unchecked")
1059 		Map<Object, Object> arrayMap = (Map<Object, Object>) array;
1060 		return arrayMap;
1061 	}
1062 
1063 	private int splitWorker(Enumeration<Object> e, Map<Object, Object> array) {
1064 		int cnt = 0;
1065 		array.clear();
1066 		while (e.hasMoreElements()) {
1067 			Object value = e.nextElement();
1068 			array.put(Long.valueOf(++cnt), toInputScalar(value));
1069 		}
1070 		array.put(0L, Long.valueOf(cnt));
1071 		return cnt;
1072 	}
1073 
1074 	/**
1075 	 * Returns the underlying {@link PartitioningReader} currently in use by
1076 	 * the active {@link InputSource}, or {@code null} if the source is not
1077 	 * stream-based.
1078 	 *
1079 	 * @return the active reader, or {@code null}
1080 	 */
1081 	public PartitioningReader getPartitioningReader() {
1082 		if (activeSource instanceof StreamInputSource) {
1083 			return ((StreamInputSource) activeSource).getPartitioningReader();
1084 		}
1085 		return null;
1086 	}
1087 
1088 	/**
1089 	 * <p>
1090 	 * Getter for the field <code>inputLine</code>.
1091 	 * </p>
1092 	 *
1093 	 * @return the current input line scalar value, or {@code null}
1094 	 */
1095 	public Object getInputLine() {
1096 		if (recordState != null) {
1097 			return recordState.getField(0);
1098 		}
1099 		return inputLine;
1100 	}
1101 
1102 	/**
1103 	 * Retrieve the current value of NF. When fields are initialized this returns
1104 	 * the number of fields in $0; otherwise 0.
1105 	 *
1106 	 * @return current NF value
1107 	 */
1108 	public Integer getNF() {
1109 		if (recordState == null) {
1110 			return Integer.valueOf(0);
1111 		}
1112 		return Integer.valueOf(recordState.getNF());
1113 	}
1114 
1115 	/**
1116 	 * Set NF to the specified value and update $0 and fields accordingly.
1117 	 *
1118 	 * @param nfObject value to assign to NF
1119 	 */
1120 	public void setNF(Object nfObject) {
1121 		jrtSetNF(nfObject);
1122 	}
1123 
1124 	/**
1125 	 * Get the current NR value as tracked by JRT.
1126 	 *
1127 	 * @return current NR
1128 	 */
1129 	public Long getNR() {
1130 		return Long.valueOf(nr);
1131 	}
1132 
1133 	/**
1134 	 * Assign NR to a specific value; also updates the VariableManager copy.
1135 	 *
1136 	 * @param value value to assign
1137 	 */
1138 	public void setNR(Object value) {
1139 		this.nr = toLong(value);
1140 	}
1141 
1142 	/**
1143 	 * Get the current FNR value as tracked by JRT.
1144 	 *
1145 	 * @return current FNR
1146 	 */
1147 	public Long getFNR() {
1148 		return Long.valueOf(fnr);
1149 	}
1150 
1151 	/**
1152 	 * Assign FNR to a specific value; also updates the VariableManager copy.
1153 	 *
1154 	 * @param value value to assign
1155 	 */
1156 	public void setFNR(Object value) {
1157 		this.fnr = toLong(value);
1158 	}
1159 
1160 	/**
1161 	 * Get FS from the VariableManager.
1162 	 *
1163 	 * @return FS value
1164 	 */
1165 	public Object getFSVar() {
1166 		return fs;
1167 	}
1168 
1169 	/**
1170 	 * Returns the current FS value as a string.
1171 	 *
1172 	 * @return current field separator
1173 	 */
1174 	public String getFSString() {
1175 		return fs;
1176 	}
1177 
1178 	/**
1179 	 * Set FS via the VariableManager.
1180 	 *
1181 	 * @param value new FS value
1182 	 */
1183 	public void setFS(Object value) {
1184 		this.fs = value == null ? "" : value.toString();
1185 	}
1186 
1187 	/**
1188 	 * Sets IGNORECASE, precomputing its truth value so regexp operations can
1189 	 * test a boolean instead of coercing the raw value on every match.
1190 	 *
1191 	 * @param value new IGNORECASE value
1192 	 */
1193 	public void setIGNORECASE(Object value) {
1194 		this.ignorecase = value == null ? Long.valueOf(0L) : value;
1195 		// gawk: IGNORECASE is active when its value is "nonzero or non-null",
1196 		// i.e. regular AWK truthiness (strnum-aware), not numeric coercion
1197 		this.ignoreCase = toBoolean(this.ignorecase);
1198 	}
1199 
1200 	/**
1201 	 * Get IGNORECASE from the VariableManager.
1202 	 *
1203 	 * @return IGNORECASE value
1204 	 */
1205 	public Object getIGNORECASEVar() {
1206 		return ignorecase;
1207 	}
1208 
1209 	/**
1210 	 * Returns whether IGNORECASE is currently nonzero, making regexp
1211 	 * operations case-insensitive. The truth value is precomputed when
1212 	 * IGNORECASE is assigned.
1213 	 *
1214 	 * @return {@code true} when IGNORECASE is nonzero
1215 	 */
1216 	public boolean isIgnoreCase() {
1217 		return ignoreCase;
1218 	}
1219 
1220 	/**
1221 	 * Returns the {@link Pattern} flags implied by the current
1222 	 * {@code IGNORECASE} setting; dynamic regexps should be compiled with
1223 	 * these flags.
1224 	 *
1225 	 * @return {@link Pattern#CASE_INSENSITIVE} when {@code IGNORECASE} is
1226 	 *         truthy, 0 otherwise
1227 	 */
1228 	public int regexpFlags() {
1229 		return ignoreCase ? Pattern.CASE_INSENSITIVE : 0;
1230 	}
1231 
1232 	/**
1233 	 * {@code sub()} functionality: replaces the first match of {@code ere} in
1234 	 * {@code orig} with {@code repl}, honoring {@code IGNORECASE}. The
1235 	 * substituted text is available through {@link #getReplaceResult()}.
1236 	 *
1237 	 * @param orig original text
1238 	 * @param repl AWK replacement text
1239 	 * @param ere regular expression
1240 	 * @return number of replacements performed (0 or 1)
1241 	 */
1242 	public int replaceFirst(String orig, String repl, String ere) {
1243 		return replace(orig, repl, ere, false);
1244 	}
1245 
1246 	/**
1247 	 * {@code gsub()} functionality: replaces every match of {@code ere} in
1248 	 * {@code orig} with {@code repl}, honoring {@code IGNORECASE}. The
1249 	 * substituted text is available through {@link #getReplaceResult()}.
1250 	 *
1251 	 * @param orig original text
1252 	 * @param repl AWK replacement text
1253 	 * @param ere regular expression
1254 	 * @return number of replacements performed
1255 	 */
1256 	public int replaceAll(String orig, String repl, String ere) {
1257 		return replace(orig, repl, ere, true);
1258 	}
1259 
1260 	private int replace(String orig, String repl, String ere, boolean global) {
1261 		replaceResult.setLength(0);
1262 		String preparedReplacement = prepareReplacement(repl, false);
1263 		Matcher matcher = dynamicPattern(ere).matcher(orig);
1264 		int count = 0;
1265 		while (matcher.find()) {
1266 			count++;
1267 			matcher.appendReplacement(replaceResult, preparedReplacement);
1268 			if (!global) {
1269 				break;
1270 			}
1271 		}
1272 		matcher.appendTail(replaceResult);
1273 		return count;
1274 	}
1275 
1276 	/**
1277 	 * Returns the text produced by the last {@link #replaceFirst} or
1278 	 * {@link #replaceAll} call.
1279 	 *
1280 	 * @return substituted text
1281 	 */
1282 	public String getReplaceResult() {
1283 		return replaceResult.toString();
1284 	}
1285 
1286 	/**
1287 	 * Evaluates the AWK match operator ({@code text ~ regexp}), honoring
1288 	 * {@code IGNORECASE} for both precompiled regexp constants and dynamic
1289 	 * expressions.
1290 	 *
1291 	 * @param text text to match
1292 	 * @param regexp precompiled {@link Pattern} or dynamic regexp text
1293 	 * @return {@code true} when the regexp matches anywhere in the text
1294 	 */
1295 	public boolean matches(String text, Object regexp) {
1296 		if (regexp instanceof Pattern) {
1297 			// find(): AWK's ~ matches anywhere, not the entire string
1298 			return caseAwarePattern((Pattern) regexp).matcher(text).find();
1299 		}
1300 		return dynamicPattern(toAwkString(regexp)).matcher(text).find();
1301 	}
1302 
1303 	/**
1304 	 * {@code match()} functionality: locates {@code ere} in {@code s} honoring
1305 	 * {@code IGNORECASE}, updating {@code RSTART} and {@code RLENGTH}.
1306 	 *
1307 	 * @param s text to search
1308 	 * @param ere regular expression
1309 	 * @return the match position ({@code RSTART}), or 0 when there is no match
1310 	 */
1311 	public int matchPosition(String s, String ere) {
1312 		Matcher matcher = dynamicPattern(ere).matcher(s);
1313 		if (matcher.find()) {
1314 			int start = matcher.start() + 1;
1315 			setRSTART(start);
1316 			setRLENGTH(matcher.end() - matcher.start());
1317 			return start;
1318 		}
1319 		setRSTART(0);
1320 		setRLENGTH(-1);
1321 		return 0;
1322 	}
1323 
1324 	/**
1325 	 * Builds the tokenizer splitting {@code input} by the given separator,
1326 	 * following AWK field-splitting rules ({@code " "} splits on whitespace
1327 	 * runs, {@code ""} splits into characters, a single character is literal)
1328 	 * and honoring {@code IGNORECASE} for regexp separators. A precompiled
1329 	 * {@link Pattern} separator (a regexp literal) is used directly.
1330 	 *
1331 	 * @param input text to split
1332 	 * @param separator field separator: precompiled pattern or text
1333 	 * @return tokenizer producing the split parts
1334 	 */
1335 	public Enumeration<Object> splitTokenizer(String input, Object separator) {
1336 		if (separator instanceof Pattern) {
1337 			return new RegexTokenizer(input, caseAwarePattern((Pattern) separator));
1338 		}
1339 		String fsString = toAwkString(separator);
1340 		if (fsString.equals(" ")) {
1341 			return new StringTokenizer(input);
1342 		}
1343 		if (fsString.isEmpty()) {
1344 			return new CharacterTokenizer(input);
1345 		}
1346 		if (fsString.length() == 1) {
1347 			char fsChar = fsString.charAt(0);
1348 			if (ignoreCase && Character.isLetter(fsChar)) {
1349 				// a letter is regex-safe, so case-insensitive splitting can
1350 				// go through the regexp path
1351 				return new RegexTokenizer(input, dynamicPattern(fsString));
1352 			}
1353 			return new SingleCharacterTokenizer(input, fsChar);
1354 		}
1355 		return new RegexTokenizer(input, dynamicPattern(fsString));
1356 	}
1357 
1358 	/**
1359 	 * Converts an AWK replacement text into a Java {@link Matcher} replacement:
1360 	 * {@code &} becomes the whole match, {@code \&} a literal ampersand, and
1361 	 * {@code $} is escaped.
1362 	 *
1363 	 * @param awkRepl AWK replacement text
1364 	 * @param backreferences whether {@code \N} denotes capture group {@code N},
1365 	 *        as in gawk's {@code gensub()}; when {@code false}, {@code \N} stays
1366 	 *        literal as in {@code sub()} and {@code gsub()}
1367 	 * @return the equivalent Java replacement string
1368 	 */
1369 	public static String prepareReplacement(String awkRepl, boolean backreferences) {
1370 		return prepareReplacement(awkRepl, backreferences ? Integer.MAX_VALUE : -1);
1371 	}
1372 
1373 	/**
1374 	 * Converts an AWK replacement text into a Java {@link Matcher} replacement,
1375 	 * resolving gensub-style backreferences against a known number of capture
1376 	 * groups: {@code \N} beyond {@code maxGroup} is replaced by the empty
1377 	 * string, as gawk does, instead of producing a group reference that would
1378 	 * make the matcher throw.
1379 	 *
1380 	 * @param awkRepl AWK replacement text
1381 	 * @param maxGroup highest valid capture group number, or a negative value
1382 	 *        to disable backreferences entirely ({@code sub()}/{@code gsub()}
1383 	 *        semantics)
1384 	 * @return the equivalent Java replacement string
1385 	 */
1386 	public static String prepareReplacement(String awkRepl, int maxGroup) {
1387 		boolean backreferences = maxGroup >= 0;
1388 		if (awkRepl == null) {
1389 			return "";
1390 		}
1391 
1392 		if ((awkRepl.indexOf('\\') == -1) && (awkRepl.indexOf('$') == -1) && (awkRepl.indexOf('&') == -1)) {
1393 			return awkRepl;
1394 		}
1395 
1396 		StringBuilder javaRepl = new StringBuilder();
1397 		for (int i = 0; i < awkRepl.length(); i++) {
1398 			char c = awkRepl.charAt(i);
1399 
1400 			if (c == '\\' && i == awkRepl.length() - 1) {
1401 				// In gensub mode a trailing backslash is a literal backslash;
1402 				// left bare it would make Matcher.appendReplacement throw. The
1403 				// sub()/gsub() mapping keeps its historical bare form.
1404 				javaRepl.append(backreferences ? "\\\\" : "\\");
1405 				continue;
1406 			}
1407 
1408 			if (c == '\\') {
1409 				i++;
1410 				c = awkRepl.charAt(i);
1411 				if (c == '&') {
1412 					javaRepl.append('&');
1413 					continue;
1414 				} else if (c == '\\') {
1415 					javaRepl.append("\\\\");
1416 					continue;
1417 				} else if (backreferences && Character.isDigit(c)) {
1418 					if (c - '0' <= maxGroup) {
1419 						javaRepl.append('$').append(c);
1420 					}
1421 					// references beyond the pattern's groups expand to the
1422 					// empty string, as in gawk
1423 					continue;
1424 				}
1425 
1426 				javaRepl.append('\\');
1427 			}
1428 
1429 			if (c == '$') {
1430 				javaRepl.append("\\$");
1431 			} else if (c == '&') {
1432 				javaRepl.append("$0");
1433 			} else {
1434 				javaRepl.append(c);
1435 			}
1436 		}
1437 
1438 		return javaRepl.toString();
1439 	}
1440 
1441 	/**
1442 	 * Returns the pattern itself, or its case-insensitive twin when
1443 	 * {@code IGNORECASE} is set. Twins are compiled once and cached here:
1444 	 * the JDK's {@link Pattern#compile(String)} performs no caching of its
1445 	 * own (every call reparses the expression), so dropping this cache would
1446 	 * recompile the regexp on every record matched against a regexp constant.
1447 	 *
1448 	 * @param pattern base pattern
1449 	 * @return pattern honoring the current {@code IGNORECASE} setting
1450 	 */
1451 	public Pattern caseAwarePattern(Pattern pattern) {
1452 		if (!ignoreCase || (pattern.flags() & Pattern.CASE_INSENSITIVE) != 0) {
1453 			return pattern;
1454 		}
1455 		if (caseInsensitivePatterns == null) {
1456 			caseInsensitivePatterns = new IdentityHashMap<Pattern, Pattern>();
1457 		}
1458 		return caseInsensitivePatterns
1459 				.computeIfAbsent(
1460 						pattern,
1461 						base -> Pattern.compile(base.pattern(), base.flags() | Pattern.CASE_INSENSITIVE));
1462 	}
1463 
1464 	/**
1465 	 * Compiles a dynamic (string) regexp with the flags implied by the current
1466 	 * {@code IGNORECASE} setting, caching compiled patterns by expression text:
1467 	 * dynamic regexps are typically reused across records (for example a
1468 	 * {@code gsub(dynstr, ...)} loop), and the JDK's
1469 	 * {@link Pattern#compile(String, int)} reparses the expression on every
1470 	 * call. Each {@code IGNORECASE} setting has its own cache; the settings
1471 	 * cannot share one because {@link Pattern#flags()} reflects inline flag
1472 	 * constructs such as {@code (?i)}, so it cannot tell apart a pattern
1473 	 * compiled under the other setting.
1474 	 *
1475 	 * @param ere dynamic regular expression text
1476 	 * @return the compiled pattern honoring the current {@code IGNORECASE}
1477 	 *         setting
1478 	 */
1479 	public Pattern dynamicPattern(String ere) {
1480 		if (dynamicPatterns == null) {
1481 			dynamicPatterns = new HashMap<String, Pattern>();
1482 			dynamicPatternsIgnoreCase = new HashMap<String, Pattern>();
1483 		}
1484 		Map<String, Pattern> cache = ignoreCase ? dynamicPatternsIgnoreCase : dynamicPatterns;
1485 		Pattern pattern = cache.get(ere);
1486 		if (pattern == null) {
1487 			if (cache.size() >= DYNAMIC_PATTERN_CACHE_LIMIT) {
1488 				cache.clear();
1489 			}
1490 			pattern = Pattern.compile(ere, regexpFlags());
1491 			cache.put(ere, pattern);
1492 		}
1493 		return pattern;
1494 	}
1495 
1496 	/**
1497 	 * Get RS from the VariableManager.
1498 	 *
1499 	 * @return RS value
1500 	 */
1501 	public Object getRSVar() {
1502 		return rs;
1503 	}
1504 
1505 	/**
1506 	 * Returns the current RS value as a string.
1507 	 *
1508 	 * @return current record separator
1509 	 */
1510 	public String getRSString() {
1511 		return rs;
1512 	}
1513 
1514 	/**
1515 	 * Set RS via the VariableManager and apply it to the current reader if any.
1516 	 *
1517 	 * @param value new RS value
1518 	 */
1519 	public void setRS(Object value) {
1520 		this.rs = value == null ? "" : value.toString();
1521 		applyRS(this.rs);
1522 	}
1523 
1524 	/**
1525 	 * Get OFS from the VariableManager.
1526 	 *
1527 	 * @return OFS value
1528 	 */
1529 	public Object getOFSVar() {
1530 		return ofs;
1531 	}
1532 
1533 	/**
1534 	 * Returns the current OFS value as a string.
1535 	 *
1536 	 * @return current output field separator
1537 	 */
1538 	public String getOFSString() {
1539 		return ofs;
1540 	}
1541 
1542 	/**
1543 	 * Set OFS via the VariableManager.
1544 	 *
1545 	 * @param value new OFS value
1546 	 */
1547 	public void setOFS(Object value) {
1548 		this.ofs = value == null ? "" : value.toString();
1549 	}
1550 
1551 	/**
1552 	 * Get ORS from the VariableManager.
1553 	 *
1554 	 * @return ORS value
1555 	 */
1556 	public Object getORSVar() {
1557 		return ors;
1558 	}
1559 
1560 	/**
1561 	 * Returns the current ORS value as a string.
1562 	 *
1563 	 * @return current output record separator
1564 	 */
1565 	public String getORSString() {
1566 		return ors;
1567 	}
1568 
1569 	/**
1570 	 * Set ORS via the VariableManager.
1571 	 *
1572 	 * @param value new ORS value
1573 	 */
1574 	public void setORS(Object value) {
1575 		this.ors = value == null ? "" : value.toString();
1576 	}
1577 
1578 	/**
1579 	 * Get RSTART tracked by JRT (1-based).
1580 	 *
1581 	 * @return current RSTART
1582 	 */
1583 	public Integer getRSTART() {
1584 		return Integer.valueOf(rstart);
1585 	}
1586 
1587 	/**
1588 	 * Set RSTART tracked by JRT (1-based) and mirror to VariableManager.
1589 	 *
1590 	 * @param value new RSTART
1591 	 */
1592 	public void setRSTART(Object value) {
1593 		this.rstart = (int) toLong(value);
1594 	}
1595 
1596 	/**
1597 	 * Get RLENGTH tracked by JRT.
1598 	 *
1599 	 * @return current RLENGTH
1600 	 */
1601 	public Integer getRLENGTH() {
1602 		return Integer.valueOf(rlength);
1603 	}
1604 
1605 	/**
1606 	 * Set RLENGTH tracked by JRT and mirror to VariableManager.
1607 	 *
1608 	 * @param value new RLENGTH
1609 	 */
1610 	public void setRLENGTH(Object value) {
1611 		this.rlength = (int) toLong(value);
1612 	}
1613 
1614 	/**
1615 	 * Get FILENAME as tracked by JRT.
1616 	 *
1617 	 * @return current FILENAME (empty string for stdin/pipe)
1618 	 */
1619 	public Object getFILENAME() {
1620 		return filename == null ? "" : filename;
1621 	}
1622 
1623 	/**
1624 	 * Set FILENAME through VariableManager and update JRT mirror.
1625 	 *
1626 	 * @param name file name to set
1627 	 */
1628 	public void setFILENAMEViaJrt(Object name) {
1629 		this.filename = normalizeRecordValue(name);
1630 	}
1631 
1632 	/**
1633 	 * Get ERRNO as tracked by JRT.
1634 	 *
1635 	 * @return current ERRNO (empty string when no input error is pending)
1636 	 */
1637 	public Object getERRNO() {
1638 		return errno == null ? "" : errno;
1639 	}
1640 
1641 	/**
1642 	 * Set ERRNO tracked by JRT.
1643 	 *
1644 	 * @param value new ERRNO value
1645 	 */
1646 	public void setERRNO(Object value) {
1647 		this.errno = normalizeRecordValue(value);
1648 	}
1649 
1650 	/**
1651 	 * Get ARGIND as tracked by JRT.
1652 	 *
1653 	 * @return ARGV index of the current input file (0 before any file is open)
1654 	 */
1655 	public Object getARGIND() {
1656 		return argind == null ? ZERO : argind;
1657 	}
1658 
1659 	/**
1660 	 * Set ARGIND tracked by JRT.
1661 	 *
1662 	 * @param value new ARGIND value
1663 	 */
1664 	public void setARGIND(Object value) {
1665 		this.argind = normalizeRecordValue(value);
1666 	}
1667 
1668 	/**
1669 	 * Get SUBSEP from the VariableManager.
1670 	 *
1671 	 * @return SUBSEP value
1672 	 */
1673 	public Object getSUBSEPVar() {
1674 		return subsep;
1675 	}
1676 
1677 	/**
1678 	 * Returns the current SUBSEP value as a string.
1679 	 *
1680 	 * @return current multidimensional-array subscript separator
1681 	 */
1682 	public String getSUBSEPString() {
1683 		return subsep;
1684 	}
1685 
1686 	/**
1687 	 * Set SUBSEP via the VariableManager.
1688 	 *
1689 	 * @param value new SUBSEP value
1690 	 */
1691 	public void setSUBSEP(Object value) {
1692 		this.subsep = value == null ? "" : value.toString();
1693 	}
1694 
1695 	/**
1696 	 * Get CONVFMT from the VariableManager.
1697 	 *
1698 	 * @return CONVFMT value
1699 	 */
1700 	public Object getCONVFMTVar() {
1701 		return convfmt;
1702 	}
1703 
1704 	/**
1705 	 * Returns the current CONVFMT value as a string.
1706 	 *
1707 	 * @return current numeric conversion format
1708 	 */
1709 	public String getCONVFMTString() {
1710 		return convfmt;
1711 	}
1712 
1713 	/**
1714 	 * Set CONVFMT via the VariableManager.
1715 	 *
1716 	 * @param value new CONVFMT value
1717 	 */
1718 	public void setCONVFMT(Object value) {
1719 		this.convfmt = value == null ? "" : value.toString();
1720 	}
1721 
1722 	/**
1723 	 * Get OFMT from the VariableManager.
1724 	 *
1725 	 * @return OFMT value
1726 	 */
1727 	public String getOFMTString() {
1728 		return ofmt;
1729 	}
1730 
1731 	/**
1732 	 * Set OFMT via the VariableManager.
1733 	 *
1734 	 * @param value new OFMT value
1735 	 */
1736 	public void setOFMT(Object value) {
1737 		this.ofmt = value == null ? "" : value.toString();
1738 	}
1739 
1740 	/**
1741 	 * Get ARGC from the VariableManager.
1742 	 *
1743 	 * @return ARGC value
1744 	 */
1745 	public Object getARGCVar() {
1746 		return vm.getARGC();
1747 	}
1748 
1749 	/**
1750 	 * Set ARGC via the VariableManager.
1751 	 *
1752 	 * @param value new ARGC value
1753 	 */
1754 	public void setARGC(Object value) {
1755 		vm.assignVariable("ARGC", value);
1756 	}
1757 
1758 	/**
1759 	 * <p>
1760 	 * Setter for the field <code>inputLine</code>.
1761 	 * </p>
1762 	 *
1763 	 * @param inputLineParam input value
1764 	 */
1765 	public void setInputLine(Object inputLineParam) {
1766 		Object inputValue = normalizeRecordValue(inputLineParam);
1767 		this.inputLine = inputValue;
1768 		recordState = new RecordState(inputValue, null);
1769 	}
1770 
1771 	/**
1772 	 * Creates an input-derived AWK scalar value.
1773 	 *
1774 	 * @param value input text
1775 	 * @return input-derived scalar value
1776 	 */
1777 	public Object toInputScalar(Object value) {
1778 		if (value instanceof String) {
1779 			return new StrNum((String) value, decimalSeparator);
1780 		}
1781 		if (value instanceof StrNum) {
1782 			return value;
1783 		}
1784 		if (value == null || value instanceof UninitializedObject) {
1785 			return new StrNum("", decimalSeparator);
1786 		}
1787 		return new StrNum(value.toString(), decimalSeparator);
1788 	}
1789 
1790 	private static Object normalizeRecordValue(Object value) {
1791 		if (value == null || value instanceof UninitializedObject) {
1792 			return "";
1793 		}
1794 		return value;
1795 	}
1796 
1797 	/**
1798 	 * Attempt to consume one record from a structured input source and expose it
1799 	 * as the current input record.
1800 	 *
1801 	 * @param source source strategy that provides records and optional
1802 	 *        pre-split fields
1803 	 * @return {@code true} if a record was consumed; {@code false} when the
1804 	 *         source is exhausted
1805 	 * @throws IOException if the source raises an I/O error
1806 	 */
1807 	public boolean consumeInput(final InputSource source) throws IOException {
1808 		Objects.requireNonNull(source, "source");
1809 		activeSource = source;
1810 		if (!source.nextRecord()) {
1811 			return false;
1812 		}
1813 
1814 		bindConsumedRecord(source);
1815 		return true;
1816 	}
1817 
1818 	/**
1819 	 * Attempt to consume one record from the current input file only, without
1820 	 * ever advancing to the next input file. Used by the per-file main input
1821 	 * loop when BEGINFILE/ENDFILE rules or {@code nextfile} are present, so
1822 	 * that the ENDFILE rules can run at each file boundary.
1823 	 * <p>
1824 	 * When the current input file could not be opened (a pending ERRNO set by
1825 	 * {@link #advanceToNextFile(InputSource)} that no {@code nextfile}
1826 	 * consumed), the usual fatal error is raised, mirroring gawk.
1827 	 * </p>
1828 	 *
1829 	 * @param source source strategy that provides records and optional
1830 	 *        pre-split fields
1831 	 * @return {@code true} if a record was consumed; {@code false} at the end
1832 	 *         of the current input file
1833 	 * @throws IOException if the source raises an I/O error
1834 	 */
1835 	public boolean consumeCurrentFileInput(final InputSource source) throws IOException {
1836 		Objects.requireNonNull(source, "source");
1837 		if (!(source instanceof StreamInputSource)) {
1838 			// Custom input sources behave as a single unnamed input file.
1839 			return consumeInput(source);
1840 		}
1841 		StreamInputSource streamSource = (StreamInputSource) source;
1842 		throwIfCurrentFileUnopened(streamSource);
1843 		activeSource = source;
1844 		if (!streamSource.nextRecordInCurrentFile()) {
1845 			return false;
1846 		}
1847 		bindConsumedRecord(source);
1848 		return true;
1849 	}
1850 
1851 	/**
1852 	 * Attempt to consume one record of the current input file only for
1853 	 * {@code getline target}, returning the input value and leaving the
1854 	 * current input record state untouched. Used instead of
1855 	 * {@link #consumeInputToTarget(InputSource)} while the per-file main
1856 	 * input loop is active, so a {@code getline} in an action never crosses a
1857 	 * file boundary behind the BEGINFILE/ENDFILE rules' back.
1858 	 *
1859 	 * @param source source strategy that provides records and optional
1860 	 *        pre-split fields
1861 	 * @return the consumed input value, or {@code null} at the end of the
1862 	 *         current input file
1863 	 * @throws IOException if the source raises an I/O error
1864 	 */
1865 	public Object consumeCurrentFileInputToTarget(final InputSource source) throws IOException {
1866 		Objects.requireNonNull(source, "source");
1867 		if (!(source instanceof StreamInputSource)) {
1868 			// Custom input sources behave as a single unnamed input file.
1869 			return consumeInputToTarget(source);
1870 		}
1871 		StreamInputSource streamSource = (StreamInputSource) source;
1872 		throwIfCurrentFileUnopened(streamSource);
1873 		activeSource = source;
1874 		materializeCurrentRecord();
1875 		if (!streamSource.nextRecordInCurrentFile()) {
1876 			return null;
1877 		}
1878 
1879 		RecordState inputState = new RecordState(source);
1880 		this.nr++;
1881 		if (countsTowardFNR(source)) {
1882 			this.fnr++;
1883 		}
1884 		return new StrNum(inputState.getRecordText(), decimalSeparator);
1885 	}
1886 
1887 	/**
1888 	 * Raises the gawk-compatible fatal error when the current input file
1889 	 * could not be opened and no BEGINFILE rule bypassed it with
1890 	 * {@code nextfile}.
1891 	 *
1892 	 * @param streamSource the main input source to check
1893 	 */
1894 	private void throwIfCurrentFileUnopened(StreamInputSource streamSource) {
1895 		String openError = streamSource.getCurrentFileOpenError();
1896 		if (openError != null) {
1897 			throw new AwkRuntimeException(
1898 					"cannot open file `" + toAwkString(getFILENAME()) + "' for reading: " + openError);
1899 		}
1900 	}
1901 
1902 	/**
1903 	 * Advance the main input to the next input file, applying pending
1904 	 * {@code name=value} command-line assignments along the way. On success,
1905 	 * FILENAME, FNR, ARGIND, and ERRNO are updated and {@code $0} is cleared,
1906 	 * so the BEGINFILE rules observe the new file. A file that cannot be
1907 	 * opened is still reported as available, with ERRNO carrying the error
1908 	 * description (gawk BEGINFILE error handling).
1909 	 *
1910 	 * @param source source strategy that provides records and optional
1911 	 *        pre-split fields
1912 	 * @return {@code true} when a new input file (or the initial stdin
1913 	 *         stream) is available; {@code false} when input is exhausted
1914 	 * @throws IOException if an I/O error occurs while traversing ARGV
1915 	 */
1916 	public boolean advanceToNextFile(final InputSource source) throws IOException {
1917 		Objects.requireNonNull(source, "source");
1918 		if (source instanceof StreamInputSource) {
1919 			return ((StreamInputSource) source).advanceToNextFile();
1920 		}
1921 		// Custom input sources behave as a single unnamed input file.
1922 		if (syntheticFilePresented) {
1923 			return false;
1924 		}
1925 		syntheticFilePresented = true;
1926 		return true;
1927 	}
1928 
1929 	/**
1930 	 * Returns whether the current input file of the given source failed to
1931 	 * open, leaving a pending error that only a {@code nextfile} statement in
1932 	 * a BEGINFILE rule may bypass.
1933 	 *
1934 	 * @param source source strategy that provides records
1935 	 * @return {@code true} when the current input file could not be opened
1936 	 */
1937 	public boolean hasPendingInputFileError(InputSource source) {
1938 		return source instanceof StreamInputSource
1939 				&& ((StreamInputSource) source).getCurrentFileOpenError() != null;
1940 	}
1941 
1942 	/**
1943 	 * Binds the record just consumed from the given source as the current
1944 	 * input record and updates the NR/FNR counters.
1945 	 *
1946 	 * @param source the source a record was just consumed from
1947 	 */
1948 	private void bindConsumedRecord(InputSource source) {
1949 		inputLine = null;
1950 		recordState = new RecordState(source);
1951 
1952 		this.nr++;
1953 		if (countsTowardFNR(source)) {
1954 			this.fnr++;
1955 		}
1956 	}
1957 
1958 	/**
1959 	 * Returns whether consuming a record from the given source advances FNR,
1960 	 * the per-file record counter. All records of the main command-line input
1961 	 * flow count, including standard input (POSIX defines FNR as the record
1962 	 * number in the <em>current</em> input file, which stdin is). For custom
1963 	 * {@link InputSource} implementations, {@link InputSource#isFromFilenameList()}
1964 	 * keeps controlling FNR, as documented.
1965 	 *
1966 	 * @param source the source a record was just consumed from
1967 	 * @return {@code true} when the record advances FNR
1968 	 */
1969 	private static boolean countsTowardFNR(InputSource source) {
1970 		return source instanceof StreamInputSource || source.isFromFilenameList();
1971 	}
1972 
1973 	/**
1974 	 * Attempt to consume one record from a structured input source for
1975 	 * {@code getline target}, returning the input value and leaving the
1976 	 * current input record state untouched.
1977 	 *
1978 	 * @param source source strategy that provides records and optional
1979 	 *        pre-split fields
1980 	 * @return the consumed input value, or {@code null} when the source is
1981 	 *         exhausted
1982 	 * @throws IOException if the source raises an I/O error
1983 	 */
1984 	public Object consumeInputToTarget(final InputSource source) throws IOException {
1985 		Objects.requireNonNull(source, "source");
1986 		activeSource = source;
1987 		materializeCurrentRecord();
1988 		if (!source.nextRecord()) {
1989 			return null;
1990 		}
1991 
1992 		RecordState inputState = new RecordState(source);
1993 		this.nr++;
1994 		if (countsTowardFNR(source)) {
1995 			this.fnr++;
1996 		}
1997 		return new StrNum(inputState.getRecordText(), decimalSeparator);
1998 	}
1999 
2000 	/**
2001 	 * Consume at most one record from a structured source for expression
2002 	 * evaluation.
2003 	 *
2004 	 * @param source source strategy that provides records and optional
2005 	 *        pre-split fields
2006 	 * @return {@code true} if a record was consumed, {@code false} otherwise
2007 	 * @throws IOException if the source raises an I/O error
2008 	 */
2009 	public boolean consumeInputForEval(InputSource source) throws IOException {
2010 		return consumeInput(source);
2011 	}
2012 
2013 	/**
2014 	 * Initialize {@code $0..$NF} from a pre-split field list.
2015 	 *
2016 	 * @param record current {@code $0} text
2017 	 * @param preFields current fields where index {@code 0} is {@code $1}
2018 	 */
2019 	protected void initializeInputFields(String record, List<String> preFields) {
2020 		recordState = new RecordState(toInputScalar(record), preFields);
2021 	}
2022 
2023 	/**
2024 	 * Splits $0 into $1, $2, etc.
2025 	 * Called when an update to $0 has occurred.
2026 	 */
2027 	public void jrtParseFields() {
2028 		RecordState state = ensureRecordStateForTextMutation();
2029 		state.ensureFieldsMaterialized();
2030 	}
2031 
2032 	/**
2033 	 * @return true if at least one input field has been initialized.
2034 	 */
2035 	public boolean hasInputFields() {
2036 		return recordState != null;
2037 	}
2038 
2039 	/**
2040 	 * Adjust the current input field list and $0 when NF is updated by the
2041 	 * AWK script. Fields are either truncated or extended with empty values
2042 	 * so that {@code NF} truly reflects the number of fields.
2043 	 *
2044 	 * @param nfObj New value for NF
2045 	 */
2046 	public void jrtSetNF(Object nfObj) {
2047 		int nf = (int) toDouble(nfObj);
2048 		if (nf < 0) {
2049 			nf = 0;
2050 		}
2051 
2052 		RecordState state = ensureRecordStateForFieldMutation();
2053 		int currentNF = state.getNF();
2054 
2055 		if (nf < currentNF) {
2056 			for (int i = currentNF; i > nf; i--) {
2057 				state.removeField(i - 1);
2058 			}
2059 		} else if (nf > currentNF) {
2060 			for (int i = currentNF + 1; i <= nf; i++) {
2061 				state.addField("");
2062 			}
2063 		}
2064 
2065 		state.markRecordTextDirty();
2066 	}
2067 
2068 	/**
2069 	 * Retrieve the contents of a particular input field.
2070 	 *
2071 	 * @param fieldnumObj Object referring to the field number.
2072 	 * @return Contents of the field.
2073 	 */
2074 	public Object jrtGetInputField(Object fieldnumObj) {
2075 		return jrtGetInputField(parseFieldNumber(fieldnumObj));
2076 	}
2077 
2078 	/**
2079 	 * <p>
2080 	 * jrtGetInputField.
2081 	 * </p>
2082 	 *
2083 	 * @param fieldnum a long
2084 	 * @return a {@link java.lang.Object} object
2085 	 */
2086 	public Object jrtGetInputField(long fieldnum) {
2087 		if (fieldnum < 0 || fieldnum > Integer.MAX_VALUE) {
2088 			throw new AwkRuntimeException("Field $(" + Long.valueOf(fieldnum) + ") is incorrect.");
2089 		}
2090 		if (recordState == null) {
2091 			return BLANK;
2092 		}
2093 		return recordState.getField((int) fieldnum);
2094 	}
2095 
2096 	/**
2097 	 * Stores value_obj into an input field.
2098 	 *
2099 	 * @param valueObj The RHS of the assignment.
2100 	 * @param fieldNum field number to update.
2101 	 * @return A string representation of valueObj.
2102 	 */
2103 	public String jrtSetInputField(Object valueObj, long fieldNum) {
2104 		if (fieldNum > Integer.MAX_VALUE) {
2105 			throw new AwkRuntimeException("Field $(" + Long.valueOf(fieldNum) + ") is incorrect.");
2106 		}
2107 		String value = valueObj == null ? "" : valueObj.toString();
2108 		int fieldIndex = (int) fieldNum;
2109 		RecordState state = ensureRecordStateForFieldMutation();
2110 		if (valueObj instanceof UninitializedObject) {
2111 			if (fieldIndex <= state.getNF()) {
2112 				state.setField(fieldIndex - 1, "");
2113 			}
2114 		} else {
2115 			while (state.getNF() < fieldIndex) {
2116 				state.addField(BLANK);
2117 			}
2118 			state.setField(fieldIndex - 1, valueObj);
2119 		}
2120 		state.markRecordTextDirty();
2121 		return value;
2122 	}
2123 
2124 	protected void rebuildDollarZeroFromFields() {
2125 		if (recordState != null) {
2126 			recordState.markRecordTextDirty();
2127 			inputLine = recordState.getField(0);
2128 		}
2129 	}
2130 
2131 	private void materializeCurrentRecord() {
2132 		if (recordState != null) {
2133 			recordState.materialize();
2134 		}
2135 	}
2136 
2137 	private RecordState ensureRecordStateForTextMutation() {
2138 		if (recordState == null) {
2139 			recordState = new RecordState(inputLine, null);
2140 		}
2141 		return recordState;
2142 	}
2143 
2144 	private RecordState ensureRecordStateForFieldMutation() {
2145 		RecordState state = ensureRecordStateForTextMutation();
2146 		state.ensureFieldsMaterialized();
2147 		return state;
2148 	}
2149 
2150 	private List<Object> sanitizeFields(List<String> rawFields) {
2151 		List<Object> copy = new ArrayList<Object>(rawFields.size());
2152 		for (String field : rawFields) {
2153 			String value = field == null ? "" : field;
2154 			copy.add(new StrNum(value, decimalSeparator));
2155 		}
2156 		return copy;
2157 	}
2158 
2159 	private List<Object> splitRecordText(String recordText, String fieldSeparator) {
2160 		List<Object> fields = new ArrayList<Object>();
2161 		if (recordText == null || recordText.isEmpty()) {
2162 			return fields;
2163 		}
2164 
2165 		Enumeration<Object> tokenizer = splitTokenizer(recordText, fieldSeparator);
2166 
2167 		while (tokenizer.hasMoreElements()) {
2168 			fields.add(new StrNum((String) tokenizer.nextElement(), decimalSeparator));
2169 		}
2170 		return fields;
2171 	}
2172 
2173 	private static String joinFieldsWithLiteralSeparator(List<Object> fields, String separator) {
2174 		StringBuilder sb = new StringBuilder();
2175 		for (int i = 0; i < fields.size(); i++) {
2176 			if (i > 0) {
2177 				sb.append(separator);
2178 			}
2179 			Object field = fields.get(i);
2180 			sb.append(field == null ? "" : field.toString());
2181 		}
2182 		return sb.toString();
2183 	}
2184 
2185 	private String rebuildRecordTextFromFields(List<Object> fields) {
2186 		return joinFieldsWithLiteralSeparator(fields, ofs);
2187 	}
2188 
2189 	private final class RecordState {
2190 
2191 		private final String fieldSeparatorAtRead;
2192 		private final InputSource source;
2193 		private String recordText;
2194 		private Object recordScalar;
2195 		private List<Object> fields;
2196 		private boolean recordTextAvailable;
2197 		private boolean fieldsAvailable;
2198 		private boolean recordTextDirty;
2199 		private boolean fieldsDirty;
2200 		private boolean recordTextLoadedFromSource;
2201 		private boolean fieldsLoadedFromSource;
2202 
2203 		private RecordState(InputSource source) {
2204 			this(null, null, source);
2205 		}
2206 
2207 		private RecordState(Object recordValue, List<String> rawFields) {
2208 			this(recordValue, rawFields, null);
2209 		}
2210 
2211 		private RecordState(Object recordValue, List<String> rawFields, InputSource source) {
2212 			this.fieldSeparatorAtRead = fs;
2213 			this.source = source;
2214 			if (recordValue != null) {
2215 				this.recordScalar = normalizeRecordValue(recordValue);
2216 				this.recordText = this.recordScalar.toString();
2217 				this.recordTextAvailable = true;
2218 			} else if (rawFields == null && source == null) {
2219 				this.recordScalar = "";
2220 				this.recordText = "";
2221 				this.recordTextAvailable = true;
2222 			}
2223 			if (rawFields != null) {
2224 				this.fields = sanitizeFields(rawFields);
2225 				this.fieldsAvailable = true;
2226 				this.fieldsDirty = false;
2227 			} else {
2228 				this.fieldsAvailable = false;
2229 				this.fieldsDirty = true;
2230 			}
2231 			this.recordTextDirty = false;
2232 		}
2233 
2234 		private void ensureFieldsMaterialized() {
2235 			if (fieldsAvailable && !fieldsDirty) {
2236 				return;
2237 			}
2238 			if (!recordTextDirty) {
2239 				loadFieldsFromSource();
2240 				if (fieldsAvailable && !fieldsDirty) {
2241 					return;
2242 				}
2243 			}
2244 			fields = splitRecordText(getRecordText(), fieldSeparatorAtRead);
2245 			fieldsAvailable = true;
2246 			fieldsDirty = false;
2247 		}
2248 
2249 		private String getRecordText() {
2250 			if (!recordTextAvailable || recordTextDirty) {
2251 				if (recordTextDirty) {
2252 					recordText = rebuildRecordTextFromFields(fields);
2253 					recordScalar = recordText;
2254 				} else {
2255 					loadRecordTextFromSource();
2256 					if (!recordTextAvailable) {
2257 						loadFieldsFromSource();
2258 						if (!fieldsAvailable) {
2259 							throw new IllegalStateException(
2260 									"InputSource must provide record text, fields, or both after nextRecord()");
2261 						}
2262 						recordText = joinFieldsWithLiteralSeparator(fields, fieldSeparatorAtRead);
2263 						recordScalar = new StrNum(recordText, decimalSeparator);
2264 					}
2265 				}
2266 				recordTextAvailable = true;
2267 				recordTextDirty = false;
2268 			}
2269 			return recordText;
2270 		}
2271 
2272 		private int getNF() {
2273 			ensureFieldsMaterialized();
2274 			return fields.size();
2275 		}
2276 
2277 		private Object getField(int fieldIndex) {
2278 			if (fieldIndex == 0) {
2279 				String value = getRecordText();
2280 				if (recordScalar == null) {
2281 					recordScalar = value;
2282 				}
2283 				return recordScalar;
2284 			}
2285 			ensureFieldsMaterialized();
2286 			int zeroBasedIndex = fieldIndex - 1;
2287 			if (zeroBasedIndex < 0 || zeroBasedIndex >= fields.size()) {
2288 				return BLANK;
2289 			}
2290 			return fields.get(zeroBasedIndex);
2291 		}
2292 
2293 		private void setField(int zeroBasedIndex, Object value) {
2294 			ensureFieldsMaterialized();
2295 			fields.set(zeroBasedIndex, normalizeFieldValue(value));
2296 			markRecordTextDirty();
2297 		}
2298 
2299 		private void addField(Object value) {
2300 			ensureFieldsMaterialized();
2301 			fields.add(normalizeFieldValue(value));
2302 			markRecordTextDirty();
2303 		}
2304 
2305 		private Object normalizeFieldValue(Object value) {
2306 			if (value == null) {
2307 				return "";
2308 			}
2309 			return value;
2310 		}
2311 
2312 		private void removeField(int zeroBasedIndex) {
2313 			ensureFieldsMaterialized();
2314 			fields.remove(zeroBasedIndex);
2315 			markRecordTextDirty();
2316 		}
2317 
2318 		private void markRecordTextDirty() {
2319 			recordTextDirty = true;
2320 			recordTextAvailable = fieldsAvailable;
2321 			recordScalar = null;
2322 		}
2323 
2324 		private void materialize() {
2325 			getRecordText();
2326 			ensureFieldsMaterialized();
2327 		}
2328 
2329 		private void loadRecordTextFromSource() {
2330 			if (source == null || recordTextLoadedFromSource) {
2331 				return;
2332 			}
2333 			recordText = source.getRecordText();
2334 			recordTextAvailable = recordText != null;
2335 			if (recordTextAvailable) {
2336 				recordScalar = new StrNum(recordText, decimalSeparator);
2337 			}
2338 			recordTextLoadedFromSource = true;
2339 		}
2340 
2341 		private void loadFieldsFromSource() {
2342 			if (source == null || fieldsLoadedFromSource) {
2343 				return;
2344 			}
2345 			List<String> rawFields = source.getFields();
2346 			fieldsLoadedFromSource = true;
2347 			if (rawFields != null) {
2348 				fields = sanitizeFields(rawFields);
2349 				fieldsAvailable = true;
2350 				fieldsDirty = false;
2351 			}
2352 		}
2353 	}
2354 
2355 	/**
2356 	 * <p>
2357 	 * jrtConsumeFileInputForGetline.
2358 	 * </p>
2359 	 *
2360 	 * @param fileNameParam a {@link java.lang.String} object
2361 	 * @return a {@link java.lang.Integer} object
2362 	 */
2363 	public Integer jrtConsumeFileInputForGetline(String fileNameParam) {
2364 		try {
2365 			if (jrtConsumeFileInput(fileNameParam)) {
2366 				return ONE;
2367 			} else {
2368 				jrtInputString = "";
2369 				return ZERO;
2370 			}
2371 		} catch (IOException ioe) {
2372 			jrtInputString = "";
2373 			return MINUS_ONE;
2374 		}
2375 	}
2376 
2377 	/**
2378 	 * Retrieve the next line of output from a command, executing
2379 	 * the command if necessary and store it to $0.
2380 	 *
2381 	 * @param cmdString The command to execute.
2382 	 * @return Integer(1) if successful, Integer(0) if no more
2383 	 *         input is available, Integer(-1) upon an IO error.
2384 	 */
2385 	public Integer jrtConsumeCommandInputForGetline(String cmdString) {
2386 		try {
2387 			if (jrtConsumeCommandInput(cmdString)) {
2388 				return ONE;
2389 			} else {
2390 				jrtInputString = "";
2391 				return ZERO;
2392 			}
2393 		} catch (IOException ioe) {
2394 			jrtInputString = "";
2395 			return MINUS_ONE;
2396 		}
2397 	}
2398 
2399 	/**
2400 	 * Retrieve $0.
2401 	 *
2402 	 * @return The contents of the $0 input field.
2403 	 */
2404 	public String jrtGetInputString() {
2405 		return jrtInputString;
2406 	}
2407 
2408 	/**
2409 	 * <p>
2410 	 * Getter for the field <code>outputFiles</code>.
2411 	 * </p>
2412 	 *
2413 	 * @return a {@link java.util.Map} object
2414 	 */
2415 	public Map<String, PrintStream> getOutputFiles() {
2416 		Map<String, PrintStream> outputFiles = new HashMap<String, PrintStream>();
2417 		for (Map.Entry<String, FileOutputState> entry : getIoState().fileOutputs.entrySet()) {
2418 			outputFiles.put(entry.getKey(), entry.getValue().sink.getPrintStream());
2419 		}
2420 		return outputFiles;
2421 	}
2422 
2423 	/**
2424 	 * Resolves the sink used by file redirection.
2425 	 *
2426 	 * @param fileNameParam target file name
2427 	 * @param append whether output should be appended
2428 	 * @return the sink that writes to the requested file
2429 	 */
2430 	protected AwkSink getFileAwkSink(String fileNameParam, boolean append) {
2431 		return getOrCreateFileOutputState(fileNameParam, append).sink;
2432 	}
2433 
2434 	/**
2435 	 * Resolves the sink used by pipe redirection.
2436 	 *
2437 	 * @param cmd command to execute
2438 	 * @return the sink connected to the process stdin
2439 	 */
2440 	protected AwkSink getPipeAwkSink(String cmd) {
2441 		return getOrCreateProcessOutputState(cmd).sink;
2442 	}
2443 
2444 	/**
2445 	 * Writes a standard AWK {@code print} operation to the default output.
2446 	 *
2447 	 * @param values values to print
2448 	 * @throws IOException if the sink cannot be written to
2449 	 */
2450 	public void printDefault(Object[] values) throws IOException {
2451 		awkSink.print(ofs, ors, ofmt, values);
2452 	}
2453 
2454 	/**
2455 	 * Writes a standard AWK {@code print} operation to a redirected file.
2456 	 *
2457 	 * @param fileNameParam target file name
2458 	 * @param append whether output should be appended
2459 	 * @param values values to print; an empty array prints {@code $0}
2460 	 * @throws IOException if the sink cannot be written to
2461 	 */
2462 	public void printToFile(String fileNameParam, boolean append, Object[] values) throws IOException {
2463 		getFileAwkSink(fileNameParam, append).print(ofs, ors, ofmt, values);
2464 	}
2465 
2466 	/**
2467 	 * Writes a standard AWK {@code print} operation to a redirected process.
2468 	 *
2469 	 * @param cmd command to execute
2470 	 * @param values values to print; an empty array prints {@code $0}
2471 	 * @throws IOException if the sink cannot be written to
2472 	 */
2473 	public void printToProcess(String cmd, Object[] values) throws IOException {
2474 		AwkSink sink = getPipeAwkSink(cmd);
2475 		sink.print(ofs, ors, ofmt, values);
2476 		sink.flush();
2477 	}
2478 
2479 	/**
2480 	 * Writes a formatted AWK output string to the specified sink.
2481 	 *
2482 	 * @param format format string passed to {@code printf}
2483 	 * @param values values supplied after the format string
2484 	 * @throws IOException if the sink cannot be written to
2485 	 */
2486 	public void printfDefault(String format, Object[] values) throws IOException {
2487 		awkSink.printf(ofs, ors, ofmt, format, values);
2488 	}
2489 
2490 	/**
2491 	 * Writes formatted AWK output to a redirected file.
2492 	 *
2493 	 * @param fileNameParam target file name
2494 	 * @param append whether output should be appended
2495 	 * @param format format string passed to {@code printf}
2496 	 * @param values values supplied after the format string
2497 	 * @throws IOException if the sink cannot be written to
2498 	 */
2499 	public void printfToFile(String fileNameParam, boolean append, String format, Object[] values)
2500 			throws IOException {
2501 		AwkSink sink = getFileAwkSink(fileNameParam, append);
2502 		sink.printf(ofs, ors, ofmt, format, values);
2503 	}
2504 
2505 	/**
2506 	 * Writes formatted AWK output to a redirected process.
2507 	 *
2508 	 * @param cmd command to execute
2509 	 * @param format format string passed to {@code printf}
2510 	 * @param values values supplied after the format string
2511 	 * @throws IOException if the sink cannot be written to
2512 	 */
2513 	public void printfToProcess(String cmd, String format, Object[] values) throws IOException {
2514 		AwkSink sink = getPipeAwkSink(cmd);
2515 		sink.printf(ofs, ors, ofmt, format, values);
2516 		sink.flush();
2517 	}
2518 
2519 	/**
2520 	 * Retrieve the PrintStream which writes to a particular file,
2521 	 * creating the PrintStream if necessary.
2522 	 *
2523 	 * @param fileNameParam The file which to write the contents of the PrintStream.
2524 	 * @param append true to append to the file, false to overwrite the file.
2525 	 * @return a {@link java.io.PrintStream} object
2526 	 */
2527 	public PrintStream jrtGetPrintStream(String fileNameParam, boolean append) {
2528 		return getFileAwkSink(fileNameParam, append).getPrintStream();
2529 	}
2530 
2531 	/**
2532 	 * <p>
2533 	 * jrtConsumeFileInput.
2534 	 * </p>
2535 	 *
2536 	 * @param fileNameParam a {@link java.lang.String} object
2537 	 * @return a boolean
2538 	 * @throws java.io.IOException if any.
2539 	 */
2540 	public boolean jrtConsumeFileInput(String fileNameParam) throws IOException {
2541 		Map<String, PartitioningReader> fileReaders = getIoState().fileReaders;
2542 		PartitioningReader pr = fileReaders.get(fileNameParam);
2543 		if (pr == null) {
2544 			try {
2545 				pr = new PartitioningReader(
2546 						new InputStreamReader(new FileInputStream(fileNameParam), StandardCharsets.UTF_8),
2547 						this.rs);
2548 				fileReaders.put(fileNameParam, pr);
2549 				this.filename = fileNameParam;
2550 			} catch (IOException ioe) {
2551 				fileReaders.remove(fileNameParam);
2552 				throw ioe;
2553 			}
2554 		}
2555 
2556 		String recordText = pr.readRecord();
2557 		if (recordText == null) {
2558 			return false;
2559 		} else {
2560 			jrtInputString = recordText;
2561 			inputLine = toInputScalar(recordText);
2562 			recordState = new RecordState(inputLine, null);
2563 			this.nr++;
2564 			return true;
2565 		}
2566 	}
2567 
2568 	private static Process spawnProcess(String cmd) throws IOException {
2569 		Process p;
2570 
2571 		if (IS_WINDOWS) {
2572 			// spawn the process using the Windows shell
2573 			ProcessBuilder pb = new ProcessBuilder("cmd.exe", "/c", cmd);
2574 			p = pb.start();
2575 		} else {
2576 			// spawn the process using the default POSIX shell
2577 			ProcessBuilder pb = new ProcessBuilder("/bin/sh", "-c", cmd);
2578 			p = pb.start();
2579 		}
2580 
2581 		return p;
2582 	}
2583 
2584 	/**
2585 	 * <p>
2586 	 * jrtConsumeCommandInput.
2587 	 * </p>
2588 	 *
2589 	 * @param cmd a {@link java.lang.String} object
2590 	 * @return a boolean
2591 	 * @throws java.io.IOException if any.
2592 	 */
2593 	public boolean jrtConsumeCommandInput(String cmd) throws IOException {
2594 		CommandInputState commandInput = getOrCreateCommandInputState(cmd);
2595 		String recordText = commandInput.reader.readRecord();
2596 		if (recordText == null) {
2597 			return false;
2598 		} else {
2599 			jrtInputString = recordText;
2600 			inputLine = toInputScalar(recordText);
2601 			recordState = new RecordState(inputLine, null);
2602 			this.nr++;
2603 			return true;
2604 		}
2605 	}
2606 
2607 	/**
2608 	 * Retrieve the PrintStream which shuttles data to stdin for a process,
2609 	 * executing the process if necessary. Threads are created to shuttle the
2610 	 * data to/from the process.
2611 	 *
2612 	 * @param cmd The command to execute.
2613 	 * @return The PrintStream which to write to provide
2614 	 *         input data to the process.
2615 	 */
2616 	public PrintStream jrtSpawnForOutput(String cmd) {
2617 		return getPipeAwkSink(cmd).getPrintStream();
2618 	}
2619 
2620 	private FileOutputState getOrCreateFileOutputState(String fileNameParam, boolean append) {
2621 		IoState state = getIoState();
2622 		FileOutputState outputState = state.fileOutputs.get(fileNameParam);
2623 		if (outputState == null) {
2624 			outputState = createFileOutputState(fileNameParam, append);
2625 			state.fileOutputs.put(fileNameParam, outputState);
2626 		}
2627 		return outputState;
2628 	}
2629 
2630 	private FileOutputState createFileOutputState(String fileNameParam, boolean append) {
2631 		try {
2632 			PrintStream printStream = new PrintStream(
2633 					new FileOutputStream(fileNameParam, append),
2634 					true,
2635 					StandardCharsets.UTF_8.name());
2636 			return new FileOutputState(new OutputStreamAwkSink(printStream, locale));
2637 		} catch (IOException ioe) {
2638 			throw new AwkRuntimeException("Cannot open " + fileNameParam + " for writing: " + ioe);
2639 		}
2640 	}
2641 
2642 	private CommandInputState getOrCreateCommandInputState(String cmd) throws IOException {
2643 		IoState state = getIoState();
2644 		CommandInputState commandInput = state.commandInputs.get(cmd);
2645 		if (commandInput == null) {
2646 			commandInput = createCommandInputState(cmd);
2647 			state.commandInputs.put(cmd, commandInput);
2648 			this.filename = "";
2649 		}
2650 		return commandInput;
2651 	}
2652 
2653 	private CommandInputState createCommandInputState(String cmd) throws IOException {
2654 		Process process = null;
2655 		Thread errorPump = null;
2656 		try {
2657 			process = spawnProcess(cmd);
2658 			process.getOutputStream().close();
2659 			errorPump = DataPump.dumpAndReturnThread(cmd + " stderr", process.getErrorStream(), error);
2660 			PartitioningReader reader = new PartitioningReader(
2661 					new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8),
2662 					this.rs);
2663 			return new CommandInputState(process, reader, errorPump);
2664 		} catch (IOException ioe) {
2665 			if (process != null) {
2666 				process.destroy();
2667 			}
2668 			joinDataPump(errorPump);
2669 			throw ioe;
2670 		}
2671 	}
2672 
2673 	private ProcessOutputState getOrCreateProcessOutputState(String cmd) {
2674 		IoState state = getIoState();
2675 		ProcessOutputState outputState = state.processOutputs.get(cmd);
2676 		if (outputState == null) {
2677 			outputState = createProcessOutputState(cmd);
2678 			state.processOutputs.put(cmd, outputState);
2679 		}
2680 		return outputState;
2681 	}
2682 
2683 	private ProcessOutputState createProcessOutputState(String cmd) {
2684 		Process process = null;
2685 		Thread stderrPump = null;
2686 		Thread stdoutPump = null;
2687 		PrintStream processOutput = null;
2688 		try {
2689 			processOutput = awkSink.getPrintStream();
2690 			process = spawnProcess(cmd);
2691 			stderrPump = DataPump.dumpAndReturnThread(cmd + " stderr", process.getErrorStream(), error);
2692 			stdoutPump = DataPump.dumpAndReturnThread(cmd + " stdout", process.getInputStream(), processOutput);
2693 			PrintStream processInput = new PrintStream(process.getOutputStream(), true, StandardCharsets.UTF_8.name());
2694 			return new ProcessOutputState(
2695 					process,
2696 					new OutputStreamAwkSink(processInput, locale),
2697 					processOutput,
2698 					stdoutPump,
2699 					stderrPump);
2700 		} catch (IOException ioe) {
2701 			if (process != null) {
2702 				process.destroy();
2703 			}
2704 			joinDataPump(stdoutPump);
2705 			joinDataPump(stderrPump);
2706 			throw new AwkRuntimeException("Can't spawn " + cmd + ": " + ioe);
2707 		}
2708 	}
2709 
2710 	/**
2711 	 * Attempt to close an open stream, whether it is
2712 	 * an input file, output file, input process, or output
2713 	 * process.
2714 	 * <p>
2715 	 * The specification did not describe AWK behavior
2716 	 * when attempting to close streams/processes with
2717 	 * the same file/command name. In this case,
2718 	 * <em>all</em> open streams with this name
2719 	 * are closed.
2720 	 *
2721 	 * @param fileNameParam The filename/command process to close.
2722 	 * @return Integer(0) upon a successful close, Integer(-1)
2723 	 *         otherwise.
2724 	 */
2725 	public Integer jrtClose(String fileNameParam) {
2726 		boolean b1 = jrtCloseFileReader(fileNameParam);
2727 		boolean b2 = jrtCloseCommandReader(fileNameParam);
2728 		boolean b3 = jrtCloseOutputFile(fileNameParam);
2729 		boolean b4 = jrtCloseOutputStream(fileNameParam);
2730 		// either close will do
2731 		return (b1 || b2 || b3 || b4) ? ZERO : MINUS_ONE;
2732 	}
2733 
2734 	/**
2735 	 * <p>
2736 	 * jrtCloseAll.
2737 	 * </p>
2738 	 */
2739 	public void jrtCloseAll() {
2740 		IoState state = ioState;
2741 		if (state == null) {
2742 			return;
2743 		}
2744 		Set<String> set = new HashSet<String>();
2745 		for (String s : state.fileReaders.keySet()) {
2746 			set.add(s);
2747 		}
2748 		for (String s : state.commandInputs.keySet()) {
2749 			set.add(s);
2750 		}
2751 		for (String s : state.fileOutputs.keySet()) {
2752 			set.add(s);
2753 		}
2754 		for (String s : state.processOutputs.keySet()) {
2755 			set.add(s);
2756 		}
2757 		for (String s : set) {
2758 			jrtClose(s);
2759 		}
2760 	}
2761 
2762 	private boolean jrtCloseOutputFile(String fileNameParam) {
2763 		IoState state = ioState;
2764 		if (state == null) {
2765 			return false;
2766 		}
2767 		FileOutputState outputState = state.fileOutputs.remove(fileNameParam);
2768 		if (outputState != null) {
2769 			outputState.sink.getPrintStream().close();
2770 		}
2771 		return outputState != null;
2772 	}
2773 
2774 	private boolean jrtCloseOutputStream(String cmd) {
2775 		IoState state = ioState;
2776 		if (state == null) {
2777 			return false;
2778 		}
2779 		ProcessOutputState outputState = state.processOutputs.remove(cmd);
2780 		if (outputState == null) {
2781 			return false;
2782 		}
2783 		outputState.sink.getPrintStream().close();
2784 		try {
2785 			// wait for the spawned process to finish to make sure
2786 			// all output has been flushed and captured
2787 			outputState.process.waitFor();
2788 			outputState.process.exitValue();
2789 		} catch (InterruptedException ie) {
2790 			Thread.currentThread().interrupt();
2791 			outputState.process.destroyForcibly();
2792 			throw new AwkRuntimeException(
2793 					"Caught exception while waiting for process exit: " + ie);
2794 		} finally {
2795 			joinDataPump(outputState.stdoutPump);
2796 			joinDataPump(outputState.stderrPump);
2797 			outputState.processOutput.flush();
2798 			error.flush();
2799 		}
2800 		return true;
2801 	}
2802 
2803 	private boolean jrtCloseFileReader(String fileNameParam) {
2804 		IoState state = ioState;
2805 		if (state == null) {
2806 			return false;
2807 		}
2808 		PartitioningReader pr = state.fileReaders.get(fileNameParam);
2809 		if (pr == null) {
2810 			return false;
2811 		}
2812 		state.fileReaders.remove(fileNameParam);
2813 		try {
2814 			pr.close();
2815 			return true;
2816 		} catch (IOException ioe) {
2817 			return false;
2818 		}
2819 	}
2820 
2821 	private boolean jrtCloseCommandReader(String cmd) {
2822 		IoState state = ioState;
2823 		if (state == null) {
2824 			return false;
2825 		}
2826 		CommandInputState commandInput = state.commandInputs.remove(cmd);
2827 		if (commandInput == null) {
2828 			return false;
2829 		}
2830 		try {
2831 			commandInput.reader.close();
2832 			try {
2833 				// wait for the process to complete so that all
2834 				// data pumped from the command is captured
2835 				commandInput.process.waitFor();
2836 				commandInput.process.exitValue();
2837 			} catch (InterruptedException ie) {
2838 				Thread.currentThread().interrupt();
2839 				commandInput.process.destroyForcibly();
2840 				throw new AwkRuntimeException(
2841 						"Caught exception while waiting for process exit: " + ie);
2842 			}
2843 			return true;
2844 		} catch (IOException ioe) {
2845 			return false;
2846 		} finally {
2847 			joinDataPump(commandInput.errorPump);
2848 			error.flush();
2849 		}
2850 	}
2851 
2852 	/**
2853 	 * Executes the command specified by cmd and waits
2854 	 * for termination, returning an Integer object
2855 	 * containing the return code.
2856 	 * stdin to this process is closed while
2857 	 * threads are created to shuttle stdout and
2858 	 * stderr of the command to stdout/stderr
2859 	 * of the calling process.
2860 	 *
2861 	 * @param cmd The command to execute.
2862 	 * @return Integer(return_code) of the created
2863 	 *         process. Integer(-1) is returned on an IO error.
2864 	 */
2865 	public Integer jrtSystem(String cmd) {
2866 		try {
2867 			PrintStream processOutput = awkSink.getPrintStream();
2868 			Process p = spawnProcess(cmd);
2869 			// no input to this process!
2870 			p.getOutputStream().close();
2871 			Thread errorPump = DataPump.dumpAndReturnThread(cmd + " stderr", p.getErrorStream(), error);
2872 			Thread outputPump = DataPump.dumpAndReturnThread(cmd + " stdout", p.getInputStream(), processOutput);
2873 			boolean interrupted = false;
2874 			int retcode;
2875 			while (true) {
2876 				try {
2877 					retcode = p.waitFor();
2878 					break;
2879 				} catch (InterruptedException ie) {
2880 					// Preserve interrupt and keep waiting so process pipes can close.
2881 					interrupted = true;
2882 				}
2883 			}
2884 			joinDataPump(outputPump);
2885 			joinDataPump(errorPump);
2886 			processOutput.flush();
2887 			error.flush();
2888 			if (interrupted) {
2889 				Thread.currentThread().interrupt();
2890 			}
2891 			return Integer.valueOf(retcode);
2892 		} catch (IOException ioe) {
2893 			return MINUS_ONE;
2894 		}
2895 	}
2896 
2897 	private static void joinDataPump(Thread pump) {
2898 		if (pump == null) {
2899 			return;
2900 		}
2901 		boolean interrupted = false;
2902 		while (true) {
2903 			try {
2904 				pump.join();
2905 				break;
2906 			} catch (InterruptedException ie) {
2907 				interrupted = true;
2908 			}
2909 		}
2910 		if (interrupted) {
2911 			Thread.currentThread().interrupt();
2912 		}
2913 	}
2914 
2915 	/**
2916 	 * <p>
2917 	 * sprintfFunctionNoCatch.
2918 	 * </p>
2919 	 *
2920 	 * @param locale a {@link java.util.Locale} object
2921 	 * @param fmtArg a {@link java.lang.String} object
2922 	 * @param arr an array of {@link java.lang.Object} objects
2923 	 * @return a {@link java.lang.String} object
2924 	 * @throws java.util.IllegalFormatException if any.
2925 	 */
2926 	public static String sprintfNoCatch(Locale locale, String fmtArg, Object... arr) throws IllegalFormatException {
2927 		return String.format(locale, fmtArg, arr);
2928 	}
2929 
2930 	/**
2931 	 * <p>
2932 	 * printfFunctionNoCatch.
2933 	 * </p>
2934 	 *
2935 	 * @param locale a {@link java.util.Locale} object
2936 	 * @param fmtArg a {@link java.lang.String} object
2937 	 * @param arr an array of {@link java.lang.Object} objects
2938 	 */
2939 	public static void printfNoCatch(Locale locale, String fmtArg, Object... arr) {
2940 		System.out.print(sprintfNoCatch(locale, fmtArg, arr));
2941 	}
2942 
2943 	/**
2944 	 * <p>
2945 	 * printfFunctionNoCatch.
2946 	 * </p>
2947 	 *
2948 	 * @param ps a {@link java.io.PrintStream} object
2949 	 * @param locale a {@link java.util.Locale} object
2950 	 * @param fmtArg a {@link java.lang.String} object
2951 	 * @param arr an array of {@link java.lang.Object} objects
2952 	 */
2953 	public static void printfNoCatch(PrintStream ps, Locale locale, String fmtArg, Object... arr) {
2954 		ps.print(sprintfNoCatch(locale, fmtArg, arr));
2955 	}
2956 
2957 	/**
2958 	 * <p>
2959 	 * substr.
2960 	 * </p>
2961 	 *
2962 	 * @param startposObj a {@link java.lang.Object} object
2963 	 * @param str a {@link java.lang.String} object
2964 	 * @return a {@link java.lang.String} object
2965 	 */
2966 	public static String substr(Object startposObj, String str) {
2967 		int startpos = (int) toDouble(startposObj);
2968 		if (startpos <= 0) {
2969 			throw new AwkRuntimeException("2nd arg to substr must be a positive integer");
2970 		}
2971 		if (startpos > str.length()) {
2972 			return "";
2973 		} else {
2974 			return str.substring(startpos - 1);
2975 		}
2976 	}
2977 
2978 	/**
2979 	 * <p>
2980 	 * substr.
2981 	 * </p>
2982 	 *
2983 	 * @param sizeObj a {@link java.lang.Object} object
2984 	 * @param startposObj a {@link java.lang.Object} object
2985 	 * @param str a {@link java.lang.String} object
2986 	 * @return a {@link java.lang.String} object
2987 	 */
2988 	public static String substr(Object sizeObj, Object startposObj, String str) {
2989 		int startpos = (int) toDouble(startposObj);
2990 		if (startpos <= 0) {
2991 			throw new AwkRuntimeException("2nd arg to substr must be a positive integer");
2992 		}
2993 		if (startpos > str.length()) {
2994 			return "";
2995 		}
2996 		int size = (int) toDouble(sizeObj);
2997 		if (size < 0) {
2998 			throw new AwkRuntimeException("3nd arg to substr must be a non-negative integer");
2999 		}
3000 		if (startpos + size > str.length()) {
3001 			return str.substring(startpos - 1);
3002 		} else {
3003 			return str.substring(startpos - 1, startpos + size - 1);
3004 		}
3005 	}
3006 
3007 	/**
3008 	 * <p>
3009 	 * timeSeed.
3010 	 * </p>
3011 	 *
3012 	 * @return a int
3013 	 */
3014 	public static int timeSeed() {
3015 		long l = new Date().getTime();
3016 		long l2 = l % (1000 * 60 * 60 * 24);
3017 		int seed = (int) l2;
3018 		return seed;
3019 	}
3020 
3021 	/**
3022 	 * <p>
3023 	 * newRandom.
3024 	 * </p>
3025 	 *
3026 	 * @param seed a int
3027 	 * @return a {@link java.util.Random} object
3028 	 */
3029 	public static BSDRandom newRandom(int seed) {
3030 		return new BSDRandom(seed);
3031 	}
3032 
3033 	/**
3034 	 * <p>
3035 	 * applyRS.
3036 	 * </p>
3037 	 *
3038 	 * @param rsObj a {@link java.lang.Object} object
3039 	 */
3040 	public void applyRS(Object rsObj) {
3041 		if (activeSource instanceof StreamInputSource) {
3042 			((StreamInputSource) activeSource).setRecordSeparator(rsObj.toString());
3043 		}
3044 	}
3045 }