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.HashSet;
37  import java.util.IllegalFormatException;
38  import java.util.List;
39  import java.util.Locale;
40  import java.util.Map;
41  import java.util.Objects;
42  import java.util.Set;
43  import java.util.StringTokenizer;
44  import java.util.regex.Matcher;
45  import java.util.regex.Pattern;
46  import io.jawk.Awk;
47  import io.jawk.intermediate.UninitializedObject;
48  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
49  
50  /**
51   * The Jawk runtime coordinator.
52   * The JRT services interpreted and compiled Jawk scripts, mainly
53   * for IO and other non-CPU bound tasks. The goal is to house
54   * service functions into a Java-compiled class rather than
55   * to hand-craft service functions in byte-code, or cut-paste
56   * compiled JVM code into the compiled AWK script. Also,
57   * since these functions are non-CPU bound, the need for
58   * inlining is reduced.
59   * <p>
60   * Variable access is achieved through the VariableManager interface.
61   * The constructor requires a VariableManager instance (which, in
62   * this case, is the compiled Jawk class itself).
63   * <p>
64   * Main services include:
65   * <ul>
66   * <li>File and command output redirection via print(f).
67   * <li>File and command input redirection via getline.
68   * <li>Most built-in AWK functions, such as system(), sprintf(), etc.
69   * <li>Automatic AWK type conversion routines.
70   * <li>IO management for input rule processing.
71   * <li>Random number engine management.
72   * <li>Input field ($0, $1, ...) management.
73   * </ul>
74   * <p>
75   * All static and non-static service methods should be package-private
76   * to the resultant AWK script class rather than public. However,
77   * the resultant script class is not in the <code>io.jawk.jrt</code> package
78   * by default, and the user may reassign the resultant script class
79   * to another package. Therefore, all accessed methods are public.
80   *
81   * @see VariableManager
82   * @author Danny Daglas
83   */
84  public class JRT {
85  
86  	private static final boolean IS_WINDOWS = System.getProperty("os.name").indexOf("Windows") >= 0;
87  
88  	private final VariableManager vm;
89  
90  	private IoState ioState;
91  	/** Output sink used for plain AWK print/printf output. */
92  	private AwkSink awkSink;
93  	/** PrintStream used for command error output */
94  	private PrintStream error;
95  	// Last input line consumed for getline-style transport.
96  	private Object inputLine = null;
97  	// Current record state ($0, $1, $2, ...).
98  	private RecordState recordState;
99  	// The currently active InputSource (set during consumeInput calls).
100 	private InputSource activeSource;
101 	private static final UninitializedObject BLANK = new UninitializedObject();
102 
103 	private static final Integer ONE = Integer.valueOf(1);
104 	private static final Integer ZERO = Integer.valueOf(0);
105 	private static final Integer MINUS_ONE = Integer.valueOf(-1);
106 	private String jrtInputString;
107 
108 	// JRT-managed special variables (runtime only)
109 	private long nr; // total record number
110 	private long fnr; // file record number
111 	private int rstart; // last match start (1-based)
112 	private int rlength; // last match length
113 	private Object filename; // current input filename scalar (or empty for stdin/pipe)
114 	private String fs; // field separator
115 	private String rs; // record separator (regexp)
116 	private String ofs; // output field separator
117 	private String ors; // output record separator
118 	private String convfmt; // number-to-string format
119 	private String ofmt; // number-to-string for output
120 	private String subsep; // subscript separator
121 	private final Locale locale; // locale for number formatting
122 	private final char decimalSeparator; // locale decimal separator for strnum recognition
123 
124 	private static final class FileOutputState {
125 
126 		private final AwkSink sink;
127 
128 		private FileOutputState(AwkSink sinkParam) {
129 			this.sink = Objects.requireNonNull(sinkParam, "sink");
130 		}
131 	}
132 
133 	private static final class CommandInputState {
134 
135 		private final Process process;
136 		private final PartitioningReader reader;
137 		private final Thread errorPump;
138 
139 		private CommandInputState(Process processParam, PartitioningReader readerParam, Thread errorPumpParam) {
140 			this.process = Objects.requireNonNull(processParam, "process");
141 			this.reader = Objects.requireNonNull(readerParam, "reader");
142 			this.errorPump = errorPumpParam;
143 		}
144 	}
145 
146 	private static final class ProcessOutputState {
147 
148 		private final Process process;
149 		private final AwkSink sink;
150 		private final PrintStream processOutput;
151 		private final Thread stdoutPump;
152 		private final Thread stderrPump;
153 
154 		private ProcessOutputState(
155 				Process processParam,
156 				AwkSink sinkParam,
157 				PrintStream processOutputParam,
158 				Thread stdoutPumpParam,
159 				Thread stderrPumpParam) {
160 			this.process = Objects.requireNonNull(processParam, "process");
161 			this.sink = Objects.requireNonNull(sinkParam, "sink");
162 			this.processOutput = Objects.requireNonNull(processOutputParam, "processOutput");
163 			this.stdoutPump = stdoutPumpParam;
164 			this.stderrPump = stderrPumpParam;
165 		}
166 	}
167 
168 	private static final class IoState {
169 
170 		private final Map<String, PartitioningReader> fileReaders = new HashMap<String, PartitioningReader>();
171 		private final Map<String, CommandInputState> commandInputs = new HashMap<String, CommandInputState>();
172 		private final Map<String, FileOutputState> fileOutputs = new HashMap<String, FileOutputState>();
173 		private final Map<String, ProcessOutputState> processOutputs = new HashMap<String, ProcessOutputState>();
174 	}
175 
176 	/**
177 	 * Create a JRT with explicit default output and error streams.
178 	 *
179 	 * @param vm The VariableManager to use with this JRT.
180 	 * @param locale The Locale to use for number formatting.
181 	 * @param awkSink default output sink used by plain AWK print operations
182 	 * @param error default error stream used for process stderr
183 	 */
184 	@SuppressFBWarnings(value = "EI_EXPOSE_REP2", justification = "JRT must hold the provided runtime collaborators for later use")
185 	public JRT(VariableManager vm, Locale locale, AwkSink awkSink, PrintStream error) {
186 		this.vm = vm;
187 		this.locale = locale == null ? Locale.US : locale;
188 		this.decimalSeparator = DecimalFormatSymbols.getInstance(this.locale).getDecimalSeparator();
189 		this.awkSink = Objects.requireNonNull(awkSink, "awkSink");
190 		this.error = error == null ? System.err : error;
191 		this.nr = 0L;
192 		this.fnr = 0L;
193 		this.rstart = 0;
194 		this.rlength = 0;
195 		this.filename = "";
196 		this.fs = Awk.DEFAULT_FS;
197 		this.rs = Awk.DEFAULT_RS;
198 		this.ofs = Awk.DEFAULT_OFS;
199 		this.ors = Awk.DEFAULT_ORS;
200 		this.convfmt = Awk.DEFAULT_CONVFMT;
201 		this.ofmt = Awk.DEFAULT_OFMT;
202 		this.subsep = Awk.DEFAULT_SUBSEP;
203 	}
204 
205 	/**
206 	 * Sets the sink used by default {@code print} and {@code printf}
207 	 * operations.
208 	 *
209 	 * @param sink output sink to use
210 	 */
211 	public void setAwkSink(AwkSink sink) {
212 		awkSink = Objects.requireNonNull(sink, "awkSink");
213 	}
214 
215 	/**
216 	 * Sets the stream used for the stderr output of spawned processes
217 	 * (e.g.&nbsp;{@code system("...")}).
218 	 *
219 	 * @param errorStream stream to receive process stderr
220 	 */
221 	public void setErrorStream(PrintStream errorStream) {
222 		this.error = Objects.requireNonNull(errorStream, "errorStream");
223 	}
224 
225 	/**
226 	 * Returns the default output sink used by {@code print} and {@code printf}.
227 	 *
228 	 * @return the current AWK sink
229 	 */
230 	public AwkSink getAwkSink() {
231 		return awkSink;
232 	}
233 
234 	/**
235 	 * Returns the locale used for number formatting in this runtime.
236 	 *
237 	 * @return the runtime locale
238 	 */
239 	public Locale getLocale() {
240 		return locale;
241 	}
242 
243 	private IoState getIoState() {
244 		if (ioState == null) {
245 			ioState = new IoState();
246 		}
247 		return ioState;
248 	}
249 
250 	/**
251 	 * Returns whether the supplied variable name is managed directly by JRT
252 	 * rather than through the AVM runtime stack.
253 	 *
254 	 * @param name variable name to inspect
255 	 * @return {@code true} when the variable is a JRT-managed special variable
256 	 */
257 	public static boolean isJrtManagedSpecialVariable(String name) {
258 		return "FS".equals(name)
259 				|| "RS".equals(name)
260 				|| "OFS".equals(name)
261 				|| "ORS".equals(name)
262 				|| "CONVFMT".equals(name)
263 				|| "OFMT".equals(name)
264 				|| "SUBSEP".equals(name)
265 				|| "FILENAME".equals(name)
266 				|| "NF".equals(name)
267 				|| "NR".equals(name)
268 				|| "FNR".equals(name)
269 				|| "ARGC".equals(name);
270 	}
271 
272 	/**
273 	 * Copies only the JRT-managed special variables from the supplied map.
274 	 *
275 	 * @param variableMap source variable map
276 	 * @return a new map containing only JRT-managed special variables
277 	 */
278 	public static Map<String, Object> copySpecialVariables(Map<String, Object> variableMap) {
279 		Map<String, Object> specialVariables = new HashMap<String, Object>();
280 		if (variableMap == null || variableMap.isEmpty()) {
281 			return specialVariables;
282 		}
283 		for (Map.Entry<String, Object> entry : variableMap.entrySet()) {
284 			if (isJrtManagedSpecialVariable(entry.getKey())) {
285 				specialVariables.put(entry.getKey(), entry.getValue());
286 			}
287 		}
288 		return specialVariables;
289 	}
290 
291 	/**
292 	 * Resets per-execution JRT state and re-applies the default runtime special
293 	 * variables for a new script or expression execution.
294 	 * <p>
295 	 * The {@code defaultFs} and {@code defaultRs} parameters allow the caller
296 	 * to configure the initial field and record separators. Other special variables
297 	 * ({@code OFS}, {@code ORS}, {@code CONVFMT}, {@code OFMT}, {@code SUBSEP})
298 	 * use their POSIX-mandated defaults (see {@link Awk} constants) which are
299 	 * platform-independent and therefore not parameterized. Platform-specific
300 	 * end-of-line handling is the responsibility of the {@link AwkSink}.
301 	 *
302 	 * @param defaultFs default field separator, or {@code null} for
303 	 *        {@link Awk#DEFAULT_FS}
304 	 * @param defaultRs default record separator
305 	 */
306 	public void prepareForExecution(String defaultFs, String defaultRs) {
307 		// Close any previously opened IO resources before resetting state.
308 		jrtCloseAll();
309 
310 		// Clear per-execution state (IO handles, counters, input state).
311 		ioState = null;
312 		inputLine = null;
313 		recordState = null;
314 		activeSource = null;
315 		jrtInputString = null;
316 		nr = 0L;
317 		fnr = 0L;
318 		rstart = 0;
319 		rlength = 0;
320 		filename = "";
321 
322 		// Apply default runtime special variables.
323 		setFS(defaultFs == null ? Awk.DEFAULT_FS : defaultFs);
324 		setRS(defaultRs);
325 		setOFS(Awk.DEFAULT_OFS);
326 		setORS(Awk.DEFAULT_ORS);
327 		setCONVFMT(Awk.DEFAULT_CONVFMT);
328 		setOFMT(Awk.DEFAULT_OFMT);
329 		setSUBSEP(Awk.DEFAULT_SUBSEP);
330 		setFILENAMEViaJrt("");
331 		setNR(0);
332 		setFNR(0);
333 		setRSTART(0);
334 		setRLENGTH(0);
335 	}
336 
337 	/**
338 	 * Assign all -v variables.
339 	 *
340 	 * @param initialVarMap A map containing all initial variable
341 	 *        names and their values.
342 	 */
343 	public final void assignInitialVariables(Map<String, Object> initialVarMap) {
344 		for (Map.Entry<String, Object> var : initialVarMap.entrySet()) {
345 			String name = var.getKey();
346 			Object value = var.getValue();
347 			if ("FS".equals(name)) {
348 				setFS(value);
349 				continue;
350 			}
351 			if ("RS".equals(name)) {
352 				setRS(value);
353 				continue;
354 			}
355 			if ("OFS".equals(name)) {
356 				setOFS(value);
357 				continue;
358 			}
359 			if ("ORS".equals(name)) {
360 				setORS(value);
361 				continue;
362 			}
363 			if ("CONVFMT".equals(name)) {
364 				setCONVFMT(value);
365 				continue;
366 			}
367 			if ("OFMT".equals(name)) {
368 				setOFMT(value);
369 				continue;
370 			}
371 			if ("SUBSEP".equals(name)) {
372 				setSUBSEP(value);
373 				continue;
374 			}
375 			if ("FILENAME".equals(name)) {
376 				setFILENAMEViaJrt(value);
377 				continue;
378 			}
379 			if ("NF".equals(name)) {
380 				setNF(value);
381 				continue;
382 			}
383 			if ("NR".equals(name)) {
384 				setNR(value);
385 				continue;
386 			}
387 			if ("FNR".equals(name)) {
388 				setFNR(value);
389 				continue;
390 			}
391 			if ("ARGC".equals(name)) {
392 				setARGC(value);
393 				continue;
394 			}
395 			vm.assignVariable(name, value);
396 		}
397 	}
398 
399 	/**
400 	 * Applies only the JRT-managed special variable assignments from the
401 	 * supplied map (FS, RS, OFS, ORS, CONVFMT, OFMT, SUBSEP, FILENAME, NF,
402 	 * NR, FNR, ARGC). Non-special variables are silently skipped because
403 	 * they require the runtime stack to be fully initialized (which happens
404 	 * during tuple execution).
405 	 *
406 	 * @param variableMap a map of variable names to values
407 	 */
408 	public final void applySpecialVariables(Map<String, Object> variableMap) {
409 		if (variableMap == null || variableMap.isEmpty()) {
410 			return;
411 		}
412 		for (Map.Entry<String, Object> var : variableMap.entrySet()) {
413 			String name = var.getKey();
414 			Object value = var.getValue();
415 			if ("FS".equals(name)) {
416 				setFS(value);
417 			} else if ("RS".equals(name)) {
418 				setRS(value);
419 			} else if ("OFS".equals(name)) {
420 				setOFS(value);
421 			} else if ("ORS".equals(name)) {
422 				setORS(value);
423 			} else if ("CONVFMT".equals(name)) {
424 				setCONVFMT(value);
425 			} else if ("OFMT".equals(name)) {
426 				setOFMT(value);
427 			} else if ("SUBSEP".equals(name)) {
428 				setSUBSEP(value);
429 			} else if ("FILENAME".equals(name)) {
430 				setFILENAMEViaJrt(value);
431 			} else if ("NF".equals(name)) {
432 				setNF(value);
433 			} else if ("NR".equals(name)) {
434 				setNR(value);
435 			} else if ("FNR".equals(name)) {
436 				setFNR(value);
437 			} else if ("ARGC".equals(name)) {
438 				setARGC(value);
439 			}
440 			// Non-special variables are skipped; they are assigned later
441 			// via the tuple instruction stream
442 		}
443 	}
444 
445 	/**
446 	 * Called by AVM/compiled modules to assign local
447 	 * environment variables to an associative array
448 	 * (in this case, to ENVIRON).
449 	 *
450 	 * @param aa The associative array to populate with
451 	 *        environment variables. The module asserts that
452 	 *        the associative array is empty prior to population.
453 	 */
454 	public static void assignEnvironmentVariables(AssocArray aa) {
455 		Map<String, String> env = System.getenv();
456 		for (Map.Entry<String, String> var : env.entrySet()) {
457 			aa.put(var.getKey(), new StrNum(var.getValue()));
458 		}
459 	}
460 
461 	/**
462 	 * Creates an AWK-managed associative array and exposes it as a plain
463 	 * {@link Map} for callers that do not need the concrete runtime type.
464 	 *
465 	 * @param sortedArrayKeys {@code true} to keep keys sorted
466 	 * @return a new AWK associative array
467 	 */
468 	public static Map<Object, Object> createAwkMap(boolean sortedArrayKeys) {
469 		return AssocArray.create(sortedArrayKeys);
470 	}
471 
472 	/**
473 	 * Checks key existence using AWK semantics when the supplied map is backed by
474 	 * an {@link AssocArray}, otherwise falling back to regular {@link Map}
475 	 * semantics.
476 	 *
477 	 * @param map map to inspect
478 	 * @param key key to look up
479 	 * @return {@code true} when the key exists
480 	 */
481 	public static boolean containsAwkKey(Map<Object, Object> map, Object key) {
482 		if (map instanceof AssocArray) {
483 			return ((AssocArray) map).isIn(key);
484 		}
485 		return map.containsKey(key);
486 	}
487 
488 	/**
489 	 * Reads a map element using AWK semantics when the supplied map is backed by
490 	 * an {@link AssocArray}. For plain {@link Map} instances, missing or
491 	 * {@code null}-valued entries are exposed as the AWK blank value so later
492 	 * expression evaluation never receives a raw {@code null}.
493 	 *
494 	 * @param map map to inspect
495 	 * @param key key to look up
496 	 * @return the stored value, or the AWK blank value when no concrete value is
497 	 *         present
498 	 */
499 	public static Object getAwkValue(Map<Object, Object> map, Object key) {
500 		if (map instanceof AssocArray) {
501 			return map.get(key);
502 		}
503 		Object value = map.get(key);
504 		return value != null ? value : BLANK;
505 	}
506 
507 	/**
508 	 * Convert Strings, Integers, and Doubles to Strings
509 	 * based on the CONVFMT variable contents and the stored Locale.
510 	 *
511 	 * @param o Object to convert.
512 	 * @return A String representation of o.
513 	 */
514 	public String toAwkString(Object o) {
515 		return AwkSink.formatOutputValue(o, this.convfmt, this.locale);
516 	}
517 
518 	/**
519 	 * Convert a String, Integer, or Double to Double.
520 	 *
521 	 * @param o Object to convert.
522 	 * @return the "double" value of o, or 0 if invalid
523 	 */
524 	public static double toDouble(final Object o) {
525 		if (o == null) {
526 			return 0;
527 		}
528 
529 		if (o instanceof Number) {
530 			return ((Number) o).doubleValue();
531 		}
532 
533 		if (o instanceof Character) {
534 			return (double) ((Character) o).charValue();
535 		}
536 
537 		if (o instanceof StrNum) {
538 			StrNum strNum = (StrNum) o;
539 			if (strNum.isNumber()) {
540 				return strNum.doubleValue();
541 			}
542 		}
543 
544 		// Try to convert the string to a number.
545 		String s = o.toString();
546 		int length = s.length();
547 
548 		// Optimization: We don't need to handle strings that are longer than 26 chars
549 		// because a Double cannot be longer than 26 chars when converted to String.
550 		if (length > 26) {
551 			length = 26;
552 		}
553 
554 		// Loop:
555 		// If convervsion fails, try with one character less.
556 		// 25fix will convert to 25 (any numeric prefix will work)
557 		while (length > 0) {
558 			try {
559 				return Double.parseDouble(s.substring(0, length));
560 			} catch (NumberFormatException nfe) {
561 				length--;
562 			}
563 		}
564 
565 		// Failed (not even with one char)
566 		return 0;
567 	}
568 
569 	/**
570 	 * Determines whether a double value actually represents a long integer
571 	 * within the limits of floating point precision.
572 	 *
573 	 * @param d the double value to examine
574 	 * @return {@code true} if {@code d} is effectively an integer
575 	 */
576 	public static boolean isActuallyLong(double d) {
577 		double r = Math.rint(d);
578 		return Math.abs(d - r) < Math.ulp(d);
579 	}
580 
581 	/**
582 	 * Convert a String, Long, or Double to Long.
583 	 *
584 	 * @param o Object to convert.
585 	 * @return the "long" value of o, or 0 if invalid
586 	 */
587 	public static long toLong(final Object o) {
588 		if (o == null) {
589 			return 0;
590 		}
591 
592 		if (o instanceof Number) {
593 			return ((Number) o).longValue();
594 		}
595 
596 		if (o instanceof Character) {
597 			return (long) ((Character) o).charValue();
598 		}
599 
600 		// Try to convert the string to a number.
601 		String s = o.toString();
602 		int length = s.length();
603 
604 		// Optimization: We don't need to handle strings that are longer than 20 chars
605 		// because a Long cannot be longer than 20 chars when converted to String.
606 		if (length > 20) {
607 			length = 20;
608 		}
609 
610 		// Loop:
611 		// If convervsion fails, try with one character less.
612 		// 25fix will convert to 25 (any numeric prefix will work)
613 		while (length > 0) {
614 			try {
615 				return Long.parseLong(s.substring(0, length));
616 			} catch (NumberFormatException nfe) {
617 				length--;
618 			}
619 		}
620 		// Failed (not even with one char)
621 		return 0;
622 	}
623 
624 	/**
625 	 * Convert a field designator to a non-negative long, raising an AWK runtime
626 	 * exception when the value is invalid.
627 	 *
628 	 * @param obj the object identifying the field (for example, the result of a
629 	 *        numeric expression)
630 	 * @return the parsed field number as a long
631 	 */
632 	public static long parseFieldNumber(Object obj) {
633 		long num = toLong(obj);
634 		if (num < 0) {
635 			throw new AwkRuntimeException(
636 					"Field $(" + obj.toString()
637 							+ ") is incorrect.");
638 		}
639 		return num;
640 	}
641 
642 	/**
643 	 * Compares two objects. Whether to employ less-than, equals, or
644 	 * greater-than checks depends on the mode chosen by the callee.
645 	 * It handles Awk variable rules and type conversion semantics.
646 	 *
647 	 * @param o1 The 1st object.
648 	 * @param o2 the 2nd object.
649 	 * @param mode
650 	 *        <ul>
651 	 *        <li>&lt; 0 - Return true if o1 &lt; o2.
652 	 *        <li>0 - Return true if o1 == o2.
653 	 *        <li>&gt; 0 - Return true if o1 &gt; o2.
654 	 *        </ul>
655 	 * @return a boolean
656 	 */
657 	public static boolean compare2(Object o1, Object o2, int mode) {
658 		if (o1 instanceof Number && o2 instanceof Number) {
659 			return compareNumbers(((Number) o1).doubleValue(), ((Number) o2).doubleValue(), mode);
660 		}
661 
662 		String o1String = o1 == null ? "" : o1.toString();
663 		String o2String = o2 == null ? "" : o2.toString();
664 
665 		if (o1 instanceof UninitializedObject) {
666 			if (isBlankOrZero(o2, o2String)) {
667 				return mode == 0;
668 			} else {
669 				return mode < 0;
670 			}
671 		}
672 		if (o2 instanceof UninitializedObject) {
673 			if (isBlankOrZero(o1, o1String)) {
674 				return mode == 0;
675 			} else {
676 				return mode > 0;
677 			}
678 		}
679 
680 		if (isNumericComparisonOperand(o1) && isNumericComparisonOperand(o2)) {
681 			return compareNumbers(getDoubleForComparison(o1), getDoubleForComparison(o2), mode);
682 		}
683 
684 		if (mode == 0) {
685 			return o1String.equals(o2String);
686 		} else if (mode < 0) {
687 			return o1String.compareTo(o2String) < 0;
688 		} else {
689 			return o1String.compareTo(o2String) > 0;
690 		}
691 	}
692 
693 	private static boolean isBlankOrZero(Object value, String stringValue) {
694 		if (value instanceof UninitializedObject) {
695 			return true;
696 		}
697 		if (value instanceof Number) {
698 			return ((Number) value).doubleValue() == 0.0D;
699 		}
700 		if (value instanceof StrNum && ((StrNum) value).isNumber()) {
701 			return ((StrNum) value).doubleValue() == 0.0D;
702 		}
703 		return "".equals(stringValue) || "0".equals(stringValue);
704 	}
705 
706 	private static boolean isNumericComparisonOperand(Object value) {
707 		return value instanceof Number || value instanceof StrNum && ((StrNum) value).isNumber();
708 	}
709 
710 	private static double getDoubleForComparison(Object value) {
711 		if (value instanceof Number) {
712 			return ((Number) value).doubleValue();
713 		}
714 		return ((StrNum) value).doubleValue();
715 	}
716 
717 	private static boolean compareNumbers(double o1Number, double o2Number, int mode) {
718 		if (mode < 0) {
719 			return o1Number < o2Number;
720 		} else if (mode == 0) {
721 			return o1Number == o2Number;
722 		} else {
723 			return o1Number > o2Number;
724 		}
725 	}
726 
727 	/**
728 	 * Converts an internal runtime scalar to the value exposed through Java APIs.
729 	 *
730 	 * @param value internal scalar value
731 	 * @return plain Java scalar value
732 	 */
733 	public static Object toJavaScalar(Object value) {
734 		if (value instanceof StrNum) {
735 			return value.toString();
736 		}
737 		if (value instanceof Double || value instanceof Float) {
738 			double number = ((Number) value).doubleValue();
739 			if (isActuallyLong(number)) {
740 				return Long.valueOf((long) Math.rint(number));
741 			}
742 		}
743 		return value;
744 	}
745 
746 	static boolean isParseableNumber(String value, char decimalSeparator) {
747 		int index = 0;
748 		int length = value.length();
749 
750 		if (length == 0) {
751 			return false;
752 		}
753 
754 		char current = value.charAt(index);
755 		if (current == '+' || current == '-') {
756 			index++;
757 			if (index == length) {
758 				return false;
759 			}
760 		}
761 
762 		boolean digitFound = false;
763 		while (index < length && value.charAt(index) >= '0' && value.charAt(index) <= '9') {
764 			index++;
765 			digitFound = true;
766 		}
767 
768 		if (index < length && value.charAt(index) == decimalSeparator) {
769 			index++;
770 			while (index < length && value.charAt(index) >= '0' && value.charAt(index) <= '9') {
771 				index++;
772 				digitFound = true;
773 			}
774 		}
775 
776 		if (!digitFound) {
777 			return false;
778 		}
779 
780 		if (index < length && (value.charAt(index) == 'e' || value.charAt(index) == 'E')) {
781 			index++;
782 			if (index < length && (value.charAt(index) == '+' || value.charAt(index) == '-')) {
783 				index++;
784 			}
785 
786 			boolean exponentDigitFound = false;
787 			while (index < length && value.charAt(index) >= '0' && value.charAt(index) <= '9') {
788 				index++;
789 				exponentDigitFound = true;
790 			}
791 			if (!exponentDigitFound) {
792 				return false;
793 			}
794 		}
795 
796 		return index == length;
797 	}
798 
799 	static String normalizeNumberForComparison(String value, char decimalSeparator) {
800 		return decimalSeparator == '.' ? value : value.replace(decimalSeparator, '.');
801 	}
802 
803 	/**
804 	 * Return an object which is numerically equivalent to
805 	 * one plus a given object. For Integers and Doubles,
806 	 * this is similar to o+1. For Strings, attempts are
807 	 * made to convert it to a double first. If the
808 	 * String does not contain a numeric prefix, 1 is returned.
809 	 *
810 	 * @param o The object to increase.
811 	 * @return {@code o + 1} if o is numeric or contains a numeric prefix;
812 	 *         otherwise, {@code 1.0}
813 	 */
814 	public static Object inc(Object o) {
815 		return toDouble(o) + 1;
816 	}
817 
818 	/**
819 	 * Return an object which is numerically equivalent to
820 	 * one minus a given object. For Integers and Doubles,
821 	 * this is similar to o-1. For Strings, attempts are
822 	 * made to convert it to a double first. If the
823 	 * String does not contain a numeric prefix, -1 is returned.
824 	 *
825 	 * @param o The object to increase.
826 	 * @return {@code o - 1} if o is numeric or contains a numeric prefix;
827 	 *         otherwise, {@code -1.0}
828 	 */
829 	public static Object dec(Object o) {
830 		return toDouble(o) - 1;
831 	}
832 
833 	// non-static to reference "inputLine"
834 	/**
835 	 * Converts an Integer, Double, String, Pattern,
836 	 * or ConditionPair to a boolean.
837 	 *
838 	 * @param o The object to convert to a boolean.
839 	 * @return For the following class types for o:
840 	 *         <ul>
841 	 *         <li><strong>Integer</strong> - o.intValue() != 0
842 	 *         <li><strong>Long</strong> - o.longValue() != 0
843 	 *         <li><strong>Double</strong> - o.doubleValue() != 0
844 	 *         <li><strong>String</strong> - o.length() &gt; 0
845 	 *         <li><strong>UninitializedObject</strong> - false
846 	 *         <li><strong>Pattern</strong> - $0 ~ o
847 	 *         </ul>
848 	 *         If o is none of these types, an error is thrown.
849 	 */
850 	public final boolean toBoolean(Object o) {
851 		boolean val;
852 		if (o instanceof Integer) {
853 			val = ((Integer) o).intValue() != 0;
854 		} else if (o instanceof Long) {
855 			val = ((Long) o).longValue() != 0;
856 		} else if (o instanceof Double) {
857 			val = ((Double) o).doubleValue() != 0;
858 		} else if (o instanceof StrNum) {
859 			StrNum strNum = (StrNum) o;
860 			val = strNum.isNumber() ? strNum.doubleValue() != 0 : strNum.toString().length() > 0;
861 		} else if (o instanceof String) {
862 			val = (o.toString().length() > 0);
863 		} else if (o instanceof UninitializedObject) {
864 			val = false;
865 		} else if (o instanceof Pattern) {
866 			// match against $0
867 			// ...
868 			Pattern pattern = (Pattern) o;
869 			Object inputField = jrtGetInputField(0);
870 			String s = inputField instanceof UninitializedObject ? "" : inputField.toString();
871 			Matcher matcher = pattern.matcher(s);
872 			val = matcher.find();
873 		} else {
874 			throw new Error("Unknown operand_stack type: " + o.getClass() + " for value " + o);
875 		}
876 		return val;
877 	}
878 
879 	/**
880 	 * Splits the string into parts separated by one or more spaces;
881 	 * blank first and last fields are eliminated.
882 	 * This conforms to the 2-argument version of AWK's split function.
883 	 *
884 	 * @param array The array to populate.
885 	 * @param string The string to split.
886 	 * @return The number of parts resulting from this split operation.
887 	 */
888 	public int split(Object array, Object string) {
889 		return splitWorker(new StringTokenizer(toAwkString(string)), toArrayMap(array));
890 	}
891 
892 	/**
893 	 * Splits the string into parts separated the regular expression fs.
894 	 * This conforms to the 3-argument version of AWK's split function.
895 	 * <p>
896 	 * If fs is blank, it behaves similar to the 2-arg version of
897 	 * AWK's split function.
898 	 *
899 	 * @param fieldSeparator Field separator regular expression.
900 	 * @param array The array to populate.
901 	 * @param string The string to split.
902 	 * @return The number of parts resulting from this split operation.
903 	 */
904 	public int split(Object fieldSeparator, Object array, Object string) {
905 		String fsString = toAwkString(fieldSeparator);
906 		if (fsString.equals(" ")) {
907 			return splitWorker(new StringTokenizer(toAwkString(string)), toArrayMap(array));
908 		} else if (fsString.equals("")) {
909 			return splitWorker(new CharacterTokenizer(toAwkString(string)), toArrayMap(array));
910 		} else if (fsString.length() == 1) {
911 			return splitWorker(
912 					new SingleCharacterTokenizer(toAwkString(string), fsString.charAt(0)),
913 					toArrayMap(array));
914 		} else {
915 			return splitWorker(new RegexTokenizer(toAwkString(string), fsString), toArrayMap(array));
916 		}
917 	}
918 
919 	private static Map<Object, Object> toArrayMap(Object array) {
920 		if (!(array instanceof Map)) {
921 			throw new IllegalArgumentException("split target must be a Map.");
922 		}
923 		@SuppressWarnings("unchecked")
924 		Map<Object, Object> arrayMap = (Map<Object, Object>) array;
925 		return arrayMap;
926 	}
927 
928 	private int splitWorker(Enumeration<Object> e, Map<Object, Object> array) {
929 		int cnt = 0;
930 		array.clear();
931 		while (e.hasMoreElements()) {
932 			Object value = e.nextElement();
933 			array.put(Long.valueOf(++cnt), toInputScalar(value));
934 		}
935 		array.put(0L, Long.valueOf(cnt));
936 		return cnt;
937 	}
938 
939 	/**
940 	 * Returns the underlying {@link PartitioningReader} currently in use by
941 	 * the active {@link InputSource}, or {@code null} if the source is not
942 	 * stream-based.
943 	 *
944 	 * @return the active reader, or {@code null}
945 	 */
946 	public PartitioningReader getPartitioningReader() {
947 		if (activeSource instanceof StreamInputSource) {
948 			return ((StreamInputSource) activeSource).getPartitioningReader();
949 		}
950 		return null;
951 	}
952 
953 	/**
954 	 * <p>
955 	 * Getter for the field <code>inputLine</code>.
956 	 * </p>
957 	 *
958 	 * @return the current input line scalar value, or {@code null}
959 	 */
960 	public Object getInputLine() {
961 		if (recordState != null) {
962 			return recordState.getField(0);
963 		}
964 		return inputLine;
965 	}
966 
967 	/**
968 	 * Retrieve the current value of NF. When fields are initialized this returns
969 	 * the number of fields in $0; otherwise 0.
970 	 *
971 	 * @return current NF value
972 	 */
973 	public Integer getNF() {
974 		if (recordState == null) {
975 			return Integer.valueOf(0);
976 		}
977 		return Integer.valueOf(recordState.getNF());
978 	}
979 
980 	/**
981 	 * Set NF to the specified value and update $0 and fields accordingly.
982 	 *
983 	 * @param nfObject value to assign to NF
984 	 */
985 	public void setNF(Object nfObject) {
986 		jrtSetNF(nfObject);
987 	}
988 
989 	/**
990 	 * Get the current NR value as tracked by JRT.
991 	 *
992 	 * @return current NR
993 	 */
994 	public Long getNR() {
995 		return Long.valueOf(nr);
996 	}
997 
998 	/**
999 	 * Assign NR to a specific value; also updates the VariableManager copy.
1000 	 *
1001 	 * @param value value to assign
1002 	 */
1003 	public void setNR(Object value) {
1004 		this.nr = toLong(value);
1005 	}
1006 
1007 	/**
1008 	 * Get the current FNR value as tracked by JRT.
1009 	 *
1010 	 * @return current FNR
1011 	 */
1012 	public Long getFNR() {
1013 		return Long.valueOf(fnr);
1014 	}
1015 
1016 	/**
1017 	 * Assign FNR to a specific value; also updates the VariableManager copy.
1018 	 *
1019 	 * @param value value to assign
1020 	 */
1021 	public void setFNR(Object value) {
1022 		this.fnr = toLong(value);
1023 	}
1024 
1025 	/**
1026 	 * Get FS from the VariableManager.
1027 	 *
1028 	 * @return FS value
1029 	 */
1030 	public Object getFSVar() {
1031 		return fs;
1032 	}
1033 
1034 	/**
1035 	 * Returns the current FS value as a string.
1036 	 *
1037 	 * @return current field separator
1038 	 */
1039 	public String getFSString() {
1040 		return fs;
1041 	}
1042 
1043 	/**
1044 	 * Set FS via the VariableManager.
1045 	 *
1046 	 * @param value new FS value
1047 	 */
1048 	public void setFS(Object value) {
1049 		this.fs = value == null ? "" : value.toString();
1050 	}
1051 
1052 	/**
1053 	 * Get RS from the VariableManager.
1054 	 *
1055 	 * @return RS value
1056 	 */
1057 	public Object getRSVar() {
1058 		return rs;
1059 	}
1060 
1061 	/**
1062 	 * Returns the current RS value as a string.
1063 	 *
1064 	 * @return current record separator
1065 	 */
1066 	public String getRSString() {
1067 		return rs;
1068 	}
1069 
1070 	/**
1071 	 * Set RS via the VariableManager and apply it to the current reader if any.
1072 	 *
1073 	 * @param value new RS value
1074 	 */
1075 	public void setRS(Object value) {
1076 		this.rs = value == null ? "" : value.toString();
1077 		applyRS(this.rs);
1078 	}
1079 
1080 	/**
1081 	 * Get OFS from the VariableManager.
1082 	 *
1083 	 * @return OFS value
1084 	 */
1085 	public Object getOFSVar() {
1086 		return ofs;
1087 	}
1088 
1089 	/**
1090 	 * Returns the current OFS value as a string.
1091 	 *
1092 	 * @return current output field separator
1093 	 */
1094 	public String getOFSString() {
1095 		return ofs;
1096 	}
1097 
1098 	/**
1099 	 * Set OFS via the VariableManager.
1100 	 *
1101 	 * @param value new OFS value
1102 	 */
1103 	public void setOFS(Object value) {
1104 		this.ofs = value == null ? "" : value.toString();
1105 	}
1106 
1107 	/**
1108 	 * Get ORS from the VariableManager.
1109 	 *
1110 	 * @return ORS value
1111 	 */
1112 	public Object getORSVar() {
1113 		return ors;
1114 	}
1115 
1116 	/**
1117 	 * Returns the current ORS value as a string.
1118 	 *
1119 	 * @return current output record separator
1120 	 */
1121 	public String getORSString() {
1122 		return ors;
1123 	}
1124 
1125 	/**
1126 	 * Set ORS via the VariableManager.
1127 	 *
1128 	 * @param value new ORS value
1129 	 */
1130 	public void setORS(Object value) {
1131 		this.ors = value == null ? "" : value.toString();
1132 	}
1133 
1134 	/**
1135 	 * Get RSTART tracked by JRT (1-based).
1136 	 *
1137 	 * @return current RSTART
1138 	 */
1139 	public Integer getRSTART() {
1140 		return Integer.valueOf(rstart);
1141 	}
1142 
1143 	/**
1144 	 * Set RSTART tracked by JRT (1-based) and mirror to VariableManager.
1145 	 *
1146 	 * @param value new RSTART
1147 	 */
1148 	public void setRSTART(Object value) {
1149 		this.rstart = (int) toLong(value);
1150 	}
1151 
1152 	/**
1153 	 * Get RLENGTH tracked by JRT.
1154 	 *
1155 	 * @return current RLENGTH
1156 	 */
1157 	public Integer getRLENGTH() {
1158 		return Integer.valueOf(rlength);
1159 	}
1160 
1161 	/**
1162 	 * Set RLENGTH tracked by JRT and mirror to VariableManager.
1163 	 *
1164 	 * @param value new RLENGTH
1165 	 */
1166 	public void setRLENGTH(Object value) {
1167 		this.rlength = (int) toLong(value);
1168 	}
1169 
1170 	/**
1171 	 * Get FILENAME as tracked by JRT.
1172 	 *
1173 	 * @return current FILENAME (empty string for stdin/pipe)
1174 	 */
1175 	public Object getFILENAME() {
1176 		return filename == null ? "" : filename;
1177 	}
1178 
1179 	/**
1180 	 * Set FILENAME through VariableManager and update JRT mirror.
1181 	 *
1182 	 * @param name file name to set
1183 	 */
1184 	public void setFILENAMEViaJrt(Object name) {
1185 		this.filename = normalizeRecordValue(name);
1186 	}
1187 
1188 	/**
1189 	 * Get SUBSEP from the VariableManager.
1190 	 *
1191 	 * @return SUBSEP value
1192 	 */
1193 	public Object getSUBSEPVar() {
1194 		return subsep;
1195 	}
1196 
1197 	/**
1198 	 * Returns the current SUBSEP value as a string.
1199 	 *
1200 	 * @return current multidimensional-array subscript separator
1201 	 */
1202 	public String getSUBSEPString() {
1203 		return subsep;
1204 	}
1205 
1206 	/**
1207 	 * Set SUBSEP via the VariableManager.
1208 	 *
1209 	 * @param value new SUBSEP value
1210 	 */
1211 	public void setSUBSEP(Object value) {
1212 		this.subsep = value == null ? "" : value.toString();
1213 	}
1214 
1215 	/**
1216 	 * Get CONVFMT from the VariableManager.
1217 	 *
1218 	 * @return CONVFMT value
1219 	 */
1220 	public Object getCONVFMTVar() {
1221 		return convfmt;
1222 	}
1223 
1224 	/**
1225 	 * Returns the current CONVFMT value as a string.
1226 	 *
1227 	 * @return current numeric conversion format
1228 	 */
1229 	public String getCONVFMTString() {
1230 		return convfmt;
1231 	}
1232 
1233 	/**
1234 	 * Set CONVFMT via the VariableManager.
1235 	 *
1236 	 * @param value new CONVFMT value
1237 	 */
1238 	public void setCONVFMT(Object value) {
1239 		this.convfmt = value == null ? "" : value.toString();
1240 	}
1241 
1242 	/**
1243 	 * Get OFMT from the VariableManager.
1244 	 *
1245 	 * @return OFMT value
1246 	 */
1247 	public String getOFMTString() {
1248 		return ofmt;
1249 	}
1250 
1251 	/**
1252 	 * Set OFMT via the VariableManager.
1253 	 *
1254 	 * @param value new OFMT value
1255 	 */
1256 	public void setOFMT(Object value) {
1257 		this.ofmt = value == null ? "" : value.toString();
1258 	}
1259 
1260 	/**
1261 	 * Get ARGC from the VariableManager.
1262 	 *
1263 	 * @return ARGC value
1264 	 */
1265 	public Object getARGCVar() {
1266 		return vm.getARGC();
1267 	}
1268 
1269 	/**
1270 	 * Set ARGC via the VariableManager.
1271 	 *
1272 	 * @param value new ARGC value
1273 	 */
1274 	public void setARGC(Object value) {
1275 		vm.assignVariable("ARGC", value);
1276 	}
1277 
1278 	/**
1279 	 * <p>
1280 	 * Setter for the field <code>inputLine</code>.
1281 	 * </p>
1282 	 *
1283 	 * @param inputLineParam input value
1284 	 */
1285 	public void setInputLine(Object inputLineParam) {
1286 		Object inputValue = normalizeRecordValue(inputLineParam);
1287 		this.inputLine = inputValue;
1288 		recordState = new RecordState(inputValue, null);
1289 	}
1290 
1291 	/**
1292 	 * Creates an input-derived AWK scalar value.
1293 	 *
1294 	 * @param value input text
1295 	 * @return input-derived scalar value
1296 	 */
1297 	public Object toInputScalar(Object value) {
1298 		if (value instanceof String) {
1299 			return new StrNum((String) value, decimalSeparator);
1300 		}
1301 		if (value instanceof StrNum) {
1302 			return value;
1303 		}
1304 		if (value == null || value instanceof UninitializedObject) {
1305 			return new StrNum("", decimalSeparator);
1306 		}
1307 		return new StrNum(value.toString(), decimalSeparator);
1308 	}
1309 
1310 	private static Object normalizeRecordValue(Object value) {
1311 		if (value == null || value instanceof UninitializedObject) {
1312 			return "";
1313 		}
1314 		return value;
1315 	}
1316 
1317 	/**
1318 	 * Attempt to consume one record from a structured input source and expose it
1319 	 * as the current input record.
1320 	 *
1321 	 * @param source source strategy that provides records and optional
1322 	 *        pre-split fields
1323 	 * @return {@code true} if a record was consumed; {@code false} when the
1324 	 *         source is exhausted
1325 	 * @throws IOException if the source raises an I/O error
1326 	 */
1327 	public boolean consumeInput(final InputSource source) throws IOException {
1328 		Objects.requireNonNull(source, "source");
1329 		activeSource = source;
1330 		if (!source.nextRecord()) {
1331 			return false;
1332 		}
1333 
1334 		inputLine = null;
1335 		recordState = new RecordState(source);
1336 
1337 		this.nr++;
1338 		if (source.isFromFilenameList()) {
1339 			this.fnr++;
1340 		}
1341 		return true;
1342 	}
1343 
1344 	/**
1345 	 * Attempt to consume one record from a structured input source for
1346 	 * {@code getline target}, returning the input value and leaving the
1347 	 * current input record state untouched.
1348 	 *
1349 	 * @param source source strategy that provides records and optional
1350 	 *        pre-split fields
1351 	 * @return the consumed input value, or {@code null} when the source is
1352 	 *         exhausted
1353 	 * @throws IOException if the source raises an I/O error
1354 	 */
1355 	public Object consumeInputToTarget(final InputSource source) throws IOException {
1356 		Objects.requireNonNull(source, "source");
1357 		activeSource = source;
1358 		materializeCurrentRecord();
1359 		if (!source.nextRecord()) {
1360 			return null;
1361 		}
1362 
1363 		RecordState inputState = new RecordState(source);
1364 		this.nr++;
1365 		if (source.isFromFilenameList()) {
1366 			this.fnr++;
1367 		}
1368 		return new StrNum(inputState.getRecordText(), decimalSeparator);
1369 	}
1370 
1371 	/**
1372 	 * Consume at most one record from a structured source for expression
1373 	 * evaluation.
1374 	 *
1375 	 * @param source source strategy that provides records and optional
1376 	 *        pre-split fields
1377 	 * @return {@code true} if a record was consumed, {@code false} otherwise
1378 	 * @throws IOException if the source raises an I/O error
1379 	 */
1380 	public boolean consumeInputForEval(InputSource source) throws IOException {
1381 		return consumeInput(source);
1382 	}
1383 
1384 	/**
1385 	 * Initialize {@code $0..$NF} from a pre-split field list.
1386 	 *
1387 	 * @param record current {@code $0} text
1388 	 * @param preFields current fields where index {@code 0} is {@code $1}
1389 	 */
1390 	protected void initializeInputFields(String record, List<String> preFields) {
1391 		recordState = new RecordState(toInputScalar(record), preFields);
1392 	}
1393 
1394 	/**
1395 	 * Splits $0 into $1, $2, etc.
1396 	 * Called when an update to $0 has occurred.
1397 	 */
1398 	public void jrtParseFields() {
1399 		RecordState state = ensureRecordStateForTextMutation();
1400 		state.ensureFieldsMaterialized();
1401 	}
1402 
1403 	/**
1404 	 * @return true if at least one input field has been initialized.
1405 	 */
1406 	public boolean hasInputFields() {
1407 		return recordState != null;
1408 	}
1409 
1410 	/**
1411 	 * Adjust the current input field list and $0 when NF is updated by the
1412 	 * AWK script. Fields are either truncated or extended with empty values
1413 	 * so that {@code NF} truly reflects the number of fields.
1414 	 *
1415 	 * @param nfObj New value for NF
1416 	 */
1417 	public void jrtSetNF(Object nfObj) {
1418 		int nf = (int) toDouble(nfObj);
1419 		if (nf < 0) {
1420 			nf = 0;
1421 		}
1422 
1423 		RecordState state = ensureRecordStateForFieldMutation();
1424 		int currentNF = state.getNF();
1425 
1426 		if (nf < currentNF) {
1427 			for (int i = currentNF; i > nf; i--) {
1428 				state.removeField(i - 1);
1429 			}
1430 		} else if (nf > currentNF) {
1431 			for (int i = currentNF + 1; i <= nf; i++) {
1432 				state.addField("");
1433 			}
1434 		}
1435 
1436 		state.markRecordTextDirty();
1437 	}
1438 
1439 	/**
1440 	 * Retrieve the contents of a particular input field.
1441 	 *
1442 	 * @param fieldnumObj Object referring to the field number.
1443 	 * @return Contents of the field.
1444 	 */
1445 	public Object jrtGetInputField(Object fieldnumObj) {
1446 		return jrtGetInputField(parseFieldNumber(fieldnumObj));
1447 	}
1448 
1449 	/**
1450 	 * <p>
1451 	 * jrtGetInputField.
1452 	 * </p>
1453 	 *
1454 	 * @param fieldnum a long
1455 	 * @return a {@link java.lang.Object} object
1456 	 */
1457 	public Object jrtGetInputField(long fieldnum) {
1458 		if (fieldnum < 0 || fieldnum > Integer.MAX_VALUE) {
1459 			throw new AwkRuntimeException("Field $(" + Long.valueOf(fieldnum) + ") is incorrect.");
1460 		}
1461 		if (recordState == null) {
1462 			return BLANK;
1463 		}
1464 		return recordState.getField((int) fieldnum);
1465 	}
1466 
1467 	/**
1468 	 * Stores value_obj into an input field.
1469 	 *
1470 	 * @param valueObj The RHS of the assignment.
1471 	 * @param fieldNum field number to update.
1472 	 * @return A string representation of valueObj.
1473 	 */
1474 	public String jrtSetInputField(Object valueObj, long fieldNum) {
1475 		if (fieldNum > Integer.MAX_VALUE) {
1476 			throw new AwkRuntimeException("Field $(" + Long.valueOf(fieldNum) + ") is incorrect.");
1477 		}
1478 		String value = valueObj == null ? "" : valueObj.toString();
1479 		int fieldIndex = (int) fieldNum;
1480 		RecordState state = ensureRecordStateForFieldMutation();
1481 		if (valueObj instanceof UninitializedObject) {
1482 			if (fieldIndex <= state.getNF()) {
1483 				state.setField(fieldIndex - 1, "");
1484 			}
1485 		} else {
1486 			while (state.getNF() < fieldIndex) {
1487 				state.addField("");
1488 			}
1489 			state.setField(fieldIndex - 1, valueObj);
1490 		}
1491 		state.markRecordTextDirty();
1492 		return value;
1493 	}
1494 
1495 	protected void rebuildDollarZeroFromFields() {
1496 		if (recordState != null) {
1497 			recordState.markRecordTextDirty();
1498 			inputLine = recordState.getField(0);
1499 		}
1500 	}
1501 
1502 	private void materializeCurrentRecord() {
1503 		if (recordState != null) {
1504 			recordState.materialize();
1505 		}
1506 	}
1507 
1508 	private RecordState ensureRecordStateForTextMutation() {
1509 		if (recordState == null) {
1510 			recordState = new RecordState(inputLine, null);
1511 		}
1512 		return recordState;
1513 	}
1514 
1515 	private RecordState ensureRecordStateForFieldMutation() {
1516 		RecordState state = ensureRecordStateForTextMutation();
1517 		state.ensureFieldsMaterialized();
1518 		return state;
1519 	}
1520 
1521 	private List<Object> sanitizeFields(List<String> rawFields) {
1522 		List<Object> copy = new ArrayList<Object>(rawFields.size());
1523 		for (String field : rawFields) {
1524 			String value = field == null ? "" : field;
1525 			copy.add(new StrNum(value, decimalSeparator));
1526 		}
1527 		return copy;
1528 	}
1529 
1530 	private List<Object> splitRecordText(String recordText, String fieldSeparator) {
1531 		List<Object> fields = new ArrayList<Object>();
1532 		if (recordText == null || recordText.isEmpty()) {
1533 			return fields;
1534 		}
1535 
1536 		Enumeration<Object> tokenizer;
1537 		if (fieldSeparator.equals(" ")) {
1538 			tokenizer = new StringTokenizer(recordText);
1539 		} else if (fieldSeparator.length() == 1) {
1540 			tokenizer = new SingleCharacterTokenizer(recordText, fieldSeparator.charAt(0));
1541 		} else if (fieldSeparator.equals("")) {
1542 			tokenizer = new CharacterTokenizer(recordText);
1543 		} else {
1544 			tokenizer = new RegexTokenizer(recordText, fieldSeparator);
1545 		}
1546 
1547 		while (tokenizer.hasMoreElements()) {
1548 			fields.add(new StrNum((String) tokenizer.nextElement(), decimalSeparator));
1549 		}
1550 		return fields;
1551 	}
1552 
1553 	private static String joinFieldsWithLiteralSeparator(List<Object> fields, String separator) {
1554 		StringBuilder sb = new StringBuilder();
1555 		for (int i = 0; i < fields.size(); i++) {
1556 			if (i > 0) {
1557 				sb.append(separator);
1558 			}
1559 			Object field = fields.get(i);
1560 			sb.append(field == null ? "" : field.toString());
1561 		}
1562 		return sb.toString();
1563 	}
1564 
1565 	private String rebuildRecordTextFromFields(List<Object> fields) {
1566 		return joinFieldsWithLiteralSeparator(fields, ofs);
1567 	}
1568 
1569 	private final class RecordState {
1570 
1571 		private final String fieldSeparatorAtRead;
1572 		private final InputSource source;
1573 		private String recordText;
1574 		private Object recordScalar;
1575 		private List<Object> fields;
1576 		private boolean recordTextAvailable;
1577 		private boolean fieldsAvailable;
1578 		private boolean recordTextDirty;
1579 		private boolean fieldsDirty;
1580 		private boolean recordTextLoadedFromSource;
1581 		private boolean fieldsLoadedFromSource;
1582 
1583 		private RecordState(InputSource source) {
1584 			this(null, null, source);
1585 		}
1586 
1587 		private RecordState(Object recordValue, List<String> rawFields) {
1588 			this(recordValue, rawFields, null);
1589 		}
1590 
1591 		private RecordState(Object recordValue, List<String> rawFields, InputSource source) {
1592 			this.fieldSeparatorAtRead = fs;
1593 			this.source = source;
1594 			if (recordValue != null) {
1595 				this.recordScalar = normalizeRecordValue(recordValue);
1596 				this.recordText = this.recordScalar.toString();
1597 				this.recordTextAvailable = true;
1598 			} else if (rawFields == null && source == null) {
1599 				this.recordScalar = "";
1600 				this.recordText = "";
1601 				this.recordTextAvailable = true;
1602 			}
1603 			if (rawFields != null) {
1604 				this.fields = sanitizeFields(rawFields);
1605 				this.fieldsAvailable = true;
1606 				this.fieldsDirty = false;
1607 			} else {
1608 				this.fieldsAvailable = false;
1609 				this.fieldsDirty = true;
1610 			}
1611 			this.recordTextDirty = false;
1612 		}
1613 
1614 		private void ensureFieldsMaterialized() {
1615 			if (fieldsAvailable && !fieldsDirty) {
1616 				return;
1617 			}
1618 			if (!recordTextDirty) {
1619 				loadFieldsFromSource();
1620 				if (fieldsAvailable && !fieldsDirty) {
1621 					return;
1622 				}
1623 			}
1624 			fields = splitRecordText(getRecordText(), fieldSeparatorAtRead);
1625 			fieldsAvailable = true;
1626 			fieldsDirty = false;
1627 		}
1628 
1629 		private String getRecordText() {
1630 			if (!recordTextAvailable || recordTextDirty) {
1631 				if (recordTextDirty) {
1632 					recordText = rebuildRecordTextFromFields(fields);
1633 					recordScalar = recordText;
1634 				} else {
1635 					loadRecordTextFromSource();
1636 					if (!recordTextAvailable) {
1637 						loadFieldsFromSource();
1638 						if (!fieldsAvailable) {
1639 							throw new IllegalStateException(
1640 									"InputSource must provide record text, fields, or both after nextRecord()");
1641 						}
1642 						recordText = joinFieldsWithLiteralSeparator(fields, fieldSeparatorAtRead);
1643 						recordScalar = new StrNum(recordText, decimalSeparator);
1644 					}
1645 				}
1646 				recordTextAvailable = true;
1647 				recordTextDirty = false;
1648 			}
1649 			return recordText;
1650 		}
1651 
1652 		private int getNF() {
1653 			ensureFieldsMaterialized();
1654 			return fields.size();
1655 		}
1656 
1657 		private Object getField(int fieldIndex) {
1658 			if (fieldIndex == 0) {
1659 				String value = getRecordText();
1660 				if (recordScalar == null) {
1661 					recordScalar = value;
1662 				}
1663 				return recordScalar;
1664 			}
1665 			ensureFieldsMaterialized();
1666 			int zeroBasedIndex = fieldIndex - 1;
1667 			if (zeroBasedIndex < 0 || zeroBasedIndex >= fields.size()) {
1668 				return BLANK;
1669 			}
1670 			return fields.get(zeroBasedIndex);
1671 		}
1672 
1673 		private void setField(int zeroBasedIndex, Object value) {
1674 			ensureFieldsMaterialized();
1675 			fields.set(zeroBasedIndex, normalizeFieldValue(value));
1676 			markRecordTextDirty();
1677 		}
1678 
1679 		private void addField(Object value) {
1680 			ensureFieldsMaterialized();
1681 			fields.add(normalizeFieldValue(value));
1682 			markRecordTextDirty();
1683 		}
1684 
1685 		private Object normalizeFieldValue(Object value) {
1686 			if (value == null || value instanceof UninitializedObject) {
1687 				return "";
1688 			}
1689 			return value;
1690 		}
1691 
1692 		private void removeField(int zeroBasedIndex) {
1693 			ensureFieldsMaterialized();
1694 			fields.remove(zeroBasedIndex);
1695 			markRecordTextDirty();
1696 		}
1697 
1698 		private void markRecordTextDirty() {
1699 			recordTextDirty = true;
1700 			recordTextAvailable = fieldsAvailable;
1701 			recordScalar = null;
1702 		}
1703 
1704 		private void materialize() {
1705 			getRecordText();
1706 			ensureFieldsMaterialized();
1707 		}
1708 
1709 		private void loadRecordTextFromSource() {
1710 			if (source == null || recordTextLoadedFromSource) {
1711 				return;
1712 			}
1713 			recordText = source.getRecordText();
1714 			recordTextAvailable = recordText != null;
1715 			if (recordTextAvailable) {
1716 				recordScalar = new StrNum(recordText, decimalSeparator);
1717 			}
1718 			recordTextLoadedFromSource = true;
1719 		}
1720 
1721 		private void loadFieldsFromSource() {
1722 			if (source == null || fieldsLoadedFromSource) {
1723 				return;
1724 			}
1725 			List<String> rawFields = source.getFields();
1726 			fieldsLoadedFromSource = true;
1727 			if (rawFields != null) {
1728 				fields = sanitizeFields(rawFields);
1729 				fieldsAvailable = true;
1730 				fieldsDirty = false;
1731 			}
1732 		}
1733 	}
1734 
1735 	/**
1736 	 * <p>
1737 	 * jrtConsumeFileInputForGetline.
1738 	 * </p>
1739 	 *
1740 	 * @param fileNameParam a {@link java.lang.String} object
1741 	 * @return a {@link java.lang.Integer} object
1742 	 */
1743 	public Integer jrtConsumeFileInputForGetline(String fileNameParam) {
1744 		try {
1745 			if (jrtConsumeFileInput(fileNameParam)) {
1746 				return ONE;
1747 			} else {
1748 				jrtInputString = "";
1749 				return ZERO;
1750 			}
1751 		} catch (IOException ioe) {
1752 			jrtInputString = "";
1753 			return MINUS_ONE;
1754 		}
1755 	}
1756 
1757 	/**
1758 	 * Retrieve the next line of output from a command, executing
1759 	 * the command if necessary and store it to $0.
1760 	 *
1761 	 * @param cmdString The command to execute.
1762 	 * @return Integer(1) if successful, Integer(0) if no more
1763 	 *         input is available, Integer(-1) upon an IO error.
1764 	 */
1765 	public Integer jrtConsumeCommandInputForGetline(String cmdString) {
1766 		try {
1767 			if (jrtConsumeCommandInput(cmdString)) {
1768 				return ONE;
1769 			} else {
1770 				jrtInputString = "";
1771 				return ZERO;
1772 			}
1773 		} catch (IOException ioe) {
1774 			jrtInputString = "";
1775 			return MINUS_ONE;
1776 		}
1777 	}
1778 
1779 	/**
1780 	 * Retrieve $0.
1781 	 *
1782 	 * @return The contents of the $0 input field.
1783 	 */
1784 	public String jrtGetInputString() {
1785 		return jrtInputString;
1786 	}
1787 
1788 	/**
1789 	 * <p>
1790 	 * Getter for the field <code>outputFiles</code>.
1791 	 * </p>
1792 	 *
1793 	 * @return a {@link java.util.Map} object
1794 	 */
1795 	public Map<String, PrintStream> getOutputFiles() {
1796 		Map<String, PrintStream> outputFiles = new HashMap<String, PrintStream>();
1797 		for (Map.Entry<String, FileOutputState> entry : getIoState().fileOutputs.entrySet()) {
1798 			outputFiles.put(entry.getKey(), entry.getValue().sink.getPrintStream());
1799 		}
1800 		return outputFiles;
1801 	}
1802 
1803 	/**
1804 	 * Resolves the sink used by file redirection.
1805 	 *
1806 	 * @param fileNameParam target file name
1807 	 * @param append whether output should be appended
1808 	 * @return the sink that writes to the requested file
1809 	 */
1810 	protected AwkSink getFileAwkSink(String fileNameParam, boolean append) {
1811 		return getOrCreateFileOutputState(fileNameParam, append).sink;
1812 	}
1813 
1814 	/**
1815 	 * Resolves the sink used by pipe redirection.
1816 	 *
1817 	 * @param cmd command to execute
1818 	 * @return the sink connected to the process stdin
1819 	 */
1820 	protected AwkSink getPipeAwkSink(String cmd) {
1821 		return getOrCreateProcessOutputState(cmd).sink;
1822 	}
1823 
1824 	/**
1825 	 * Writes a standard AWK {@code print} operation to the default output.
1826 	 *
1827 	 * @param values values to print
1828 	 * @throws IOException if the sink cannot be written to
1829 	 */
1830 	public void printDefault(Object[] values) throws IOException {
1831 		awkSink.print(ofs, ors, ofmt, values);
1832 	}
1833 
1834 	/**
1835 	 * Writes a standard AWK {@code print} operation to a redirected file.
1836 	 *
1837 	 * @param fileNameParam target file name
1838 	 * @param append whether output should be appended
1839 	 * @param values values to print; an empty array prints {@code $0}
1840 	 * @throws IOException if the sink cannot be written to
1841 	 */
1842 	public void printToFile(String fileNameParam, boolean append, Object[] values) throws IOException {
1843 		getFileAwkSink(fileNameParam, append).print(ofs, ors, ofmt, values);
1844 	}
1845 
1846 	/**
1847 	 * Writes a standard AWK {@code print} operation to a redirected process.
1848 	 *
1849 	 * @param cmd command to execute
1850 	 * @param values values to print; an empty array prints {@code $0}
1851 	 * @throws IOException if the sink cannot be written to
1852 	 */
1853 	public void printToProcess(String cmd, Object[] values) throws IOException {
1854 		AwkSink sink = getPipeAwkSink(cmd);
1855 		sink.print(ofs, ors, ofmt, values);
1856 		sink.flush();
1857 	}
1858 
1859 	/**
1860 	 * Writes a formatted AWK output string to the specified sink.
1861 	 *
1862 	 * @param format format string passed to {@code printf}
1863 	 * @param values values supplied after the format string
1864 	 * @throws IOException if the sink cannot be written to
1865 	 */
1866 	public void printfDefault(String format, Object[] values) throws IOException {
1867 		awkSink.printf(ofs, ors, ofmt, format, values);
1868 	}
1869 
1870 	/**
1871 	 * Writes formatted AWK output to a redirected file.
1872 	 *
1873 	 * @param fileNameParam target file name
1874 	 * @param append whether output should be appended
1875 	 * @param format format string passed to {@code printf}
1876 	 * @param values values supplied after the format string
1877 	 * @throws IOException if the sink cannot be written to
1878 	 */
1879 	public void printfToFile(String fileNameParam, boolean append, String format, Object[] values)
1880 			throws IOException {
1881 		AwkSink sink = getFileAwkSink(fileNameParam, append);
1882 		sink.printf(ofs, ors, ofmt, format, values);
1883 	}
1884 
1885 	/**
1886 	 * Writes formatted AWK output to a redirected process.
1887 	 *
1888 	 * @param cmd command to execute
1889 	 * @param format format string passed to {@code printf}
1890 	 * @param values values supplied after the format string
1891 	 * @throws IOException if the sink cannot be written to
1892 	 */
1893 	public void printfToProcess(String cmd, String format, Object[] values) throws IOException {
1894 		AwkSink sink = getPipeAwkSink(cmd);
1895 		sink.printf(ofs, ors, ofmt, format, values);
1896 		sink.flush();
1897 	}
1898 
1899 	/**
1900 	 * Retrieve the PrintStream which writes to a particular file,
1901 	 * creating the PrintStream if necessary.
1902 	 *
1903 	 * @param fileNameParam The file which to write the contents of the PrintStream.
1904 	 * @param append true to append to the file, false to overwrite the file.
1905 	 * @return a {@link java.io.PrintStream} object
1906 	 */
1907 	public PrintStream jrtGetPrintStream(String fileNameParam, boolean append) {
1908 		return getFileAwkSink(fileNameParam, append).getPrintStream();
1909 	}
1910 
1911 	/**
1912 	 * <p>
1913 	 * jrtConsumeFileInput.
1914 	 * </p>
1915 	 *
1916 	 * @param fileNameParam a {@link java.lang.String} object
1917 	 * @return a boolean
1918 	 * @throws java.io.IOException if any.
1919 	 */
1920 	public boolean jrtConsumeFileInput(String fileNameParam) throws IOException {
1921 		Map<String, PartitioningReader> fileReaders = getIoState().fileReaders;
1922 		PartitioningReader pr = fileReaders.get(fileNameParam);
1923 		if (pr == null) {
1924 			try {
1925 				pr = new PartitioningReader(
1926 						new InputStreamReader(new FileInputStream(fileNameParam), StandardCharsets.UTF_8),
1927 						this.rs);
1928 				fileReaders.put(fileNameParam, pr);
1929 				this.filename = fileNameParam;
1930 			} catch (IOException ioe) {
1931 				fileReaders.remove(fileNameParam);
1932 				throw ioe;
1933 			}
1934 		}
1935 
1936 		String recordText = pr.readRecord();
1937 		if (recordText == null) {
1938 			return false;
1939 		} else {
1940 			jrtInputString = recordText;
1941 			inputLine = toInputScalar(recordText);
1942 			recordState = new RecordState(inputLine, null);
1943 			this.nr++;
1944 			return true;
1945 		}
1946 	}
1947 
1948 	private static Process spawnProcess(String cmd) throws IOException {
1949 		Process p;
1950 
1951 		if (IS_WINDOWS) {
1952 			// spawn the process using the Windows shell
1953 			ProcessBuilder pb = new ProcessBuilder("cmd.exe", "/c", cmd);
1954 			p = pb.start();
1955 		} else {
1956 			// spawn the process using the default POSIX shell
1957 			ProcessBuilder pb = new ProcessBuilder("/bin/sh", "-c", cmd);
1958 			p = pb.start();
1959 		}
1960 
1961 		return p;
1962 	}
1963 
1964 	/**
1965 	 * <p>
1966 	 * jrtConsumeCommandInput.
1967 	 * </p>
1968 	 *
1969 	 * @param cmd a {@link java.lang.String} object
1970 	 * @return a boolean
1971 	 * @throws java.io.IOException if any.
1972 	 */
1973 	public boolean jrtConsumeCommandInput(String cmd) throws IOException {
1974 		CommandInputState commandInput = getOrCreateCommandInputState(cmd);
1975 		String recordText = commandInput.reader.readRecord();
1976 		if (recordText == null) {
1977 			return false;
1978 		} else {
1979 			jrtInputString = recordText;
1980 			inputLine = toInputScalar(recordText);
1981 			recordState = new RecordState(inputLine, null);
1982 			this.nr++;
1983 			return true;
1984 		}
1985 	}
1986 
1987 	/**
1988 	 * Retrieve the PrintStream which shuttles data to stdin for a process,
1989 	 * executing the process if necessary. Threads are created to shuttle the
1990 	 * data to/from the process.
1991 	 *
1992 	 * @param cmd The command to execute.
1993 	 * @return The PrintStream which to write to provide
1994 	 *         input data to the process.
1995 	 */
1996 	public PrintStream jrtSpawnForOutput(String cmd) {
1997 		return getPipeAwkSink(cmd).getPrintStream();
1998 	}
1999 
2000 	private FileOutputState getOrCreateFileOutputState(String fileNameParam, boolean append) {
2001 		IoState state = getIoState();
2002 		FileOutputState outputState = state.fileOutputs.get(fileNameParam);
2003 		if (outputState == null) {
2004 			outputState = createFileOutputState(fileNameParam, append);
2005 			state.fileOutputs.put(fileNameParam, outputState);
2006 		}
2007 		return outputState;
2008 	}
2009 
2010 	private FileOutputState createFileOutputState(String fileNameParam, boolean append) {
2011 		try {
2012 			PrintStream printStream = new PrintStream(
2013 					new FileOutputStream(fileNameParam, append),
2014 					true,
2015 					StandardCharsets.UTF_8.name());
2016 			return new FileOutputState(new OutputStreamAwkSink(printStream, locale));
2017 		} catch (IOException ioe) {
2018 			throw new AwkRuntimeException("Cannot open " + fileNameParam + " for writing: " + ioe);
2019 		}
2020 	}
2021 
2022 	private CommandInputState getOrCreateCommandInputState(String cmd) throws IOException {
2023 		IoState state = getIoState();
2024 		CommandInputState commandInput = state.commandInputs.get(cmd);
2025 		if (commandInput == null) {
2026 			commandInput = createCommandInputState(cmd);
2027 			state.commandInputs.put(cmd, commandInput);
2028 			this.filename = "";
2029 		}
2030 		return commandInput;
2031 	}
2032 
2033 	private CommandInputState createCommandInputState(String cmd) throws IOException {
2034 		Process process = null;
2035 		Thread errorPump = null;
2036 		try {
2037 			process = spawnProcess(cmd);
2038 			process.getOutputStream().close();
2039 			errorPump = DataPump.dumpAndReturnThread(cmd + " stderr", process.getErrorStream(), error);
2040 			PartitioningReader reader = new PartitioningReader(
2041 					new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8),
2042 					this.rs);
2043 			return new CommandInputState(process, reader, errorPump);
2044 		} catch (IOException ioe) {
2045 			if (process != null) {
2046 				process.destroy();
2047 			}
2048 			joinDataPump(errorPump);
2049 			throw ioe;
2050 		}
2051 	}
2052 
2053 	private ProcessOutputState getOrCreateProcessOutputState(String cmd) {
2054 		IoState state = getIoState();
2055 		ProcessOutputState outputState = state.processOutputs.get(cmd);
2056 		if (outputState == null) {
2057 			outputState = createProcessOutputState(cmd);
2058 			state.processOutputs.put(cmd, outputState);
2059 		}
2060 		return outputState;
2061 	}
2062 
2063 	private ProcessOutputState createProcessOutputState(String cmd) {
2064 		Process process = null;
2065 		Thread stderrPump = null;
2066 		Thread stdoutPump = null;
2067 		PrintStream processOutput = null;
2068 		try {
2069 			processOutput = awkSink.getPrintStream();
2070 			process = spawnProcess(cmd);
2071 			stderrPump = DataPump.dumpAndReturnThread(cmd + " stderr", process.getErrorStream(), error);
2072 			stdoutPump = DataPump.dumpAndReturnThread(cmd + " stdout", process.getInputStream(), processOutput);
2073 			PrintStream processInput = new PrintStream(process.getOutputStream(), true, StandardCharsets.UTF_8.name());
2074 			return new ProcessOutputState(
2075 					process,
2076 					new OutputStreamAwkSink(processInput, locale),
2077 					processOutput,
2078 					stdoutPump,
2079 					stderrPump);
2080 		} catch (IOException ioe) {
2081 			if (process != null) {
2082 				process.destroy();
2083 			}
2084 			joinDataPump(stdoutPump);
2085 			joinDataPump(stderrPump);
2086 			throw new AwkRuntimeException("Can't spawn " + cmd + ": " + ioe);
2087 		}
2088 	}
2089 
2090 	/**
2091 	 * Attempt to close an open stream, whether it is
2092 	 * an input file, output file, input process, or output
2093 	 * process.
2094 	 * <p>
2095 	 * The specification did not describe AWK behavior
2096 	 * when attempting to close streams/processes with
2097 	 * the same file/command name. In this case,
2098 	 * <em>all</em> open streams with this name
2099 	 * are closed.
2100 	 *
2101 	 * @param fileNameParam The filename/command process to close.
2102 	 * @return Integer(0) upon a successful close, Integer(-1)
2103 	 *         otherwise.
2104 	 */
2105 	public Integer jrtClose(String fileNameParam) {
2106 		boolean b1 = jrtCloseFileReader(fileNameParam);
2107 		boolean b2 = jrtCloseCommandReader(fileNameParam);
2108 		boolean b3 = jrtCloseOutputFile(fileNameParam);
2109 		boolean b4 = jrtCloseOutputStream(fileNameParam);
2110 		// either close will do
2111 		return (b1 || b2 || b3 || b4) ? ZERO : MINUS_ONE;
2112 	}
2113 
2114 	/**
2115 	 * <p>
2116 	 * jrtCloseAll.
2117 	 * </p>
2118 	 */
2119 	public void jrtCloseAll() {
2120 		IoState state = ioState;
2121 		if (state == null) {
2122 			return;
2123 		}
2124 		Set<String> set = new HashSet<String>();
2125 		for (String s : state.fileReaders.keySet()) {
2126 			set.add(s);
2127 		}
2128 		for (String s : state.commandInputs.keySet()) {
2129 			set.add(s);
2130 		}
2131 		for (String s : state.fileOutputs.keySet()) {
2132 			set.add(s);
2133 		}
2134 		for (String s : state.processOutputs.keySet()) {
2135 			set.add(s);
2136 		}
2137 		for (String s : set) {
2138 			jrtClose(s);
2139 		}
2140 	}
2141 
2142 	private boolean jrtCloseOutputFile(String fileNameParam) {
2143 		IoState state = ioState;
2144 		if (state == null) {
2145 			return false;
2146 		}
2147 		FileOutputState outputState = state.fileOutputs.remove(fileNameParam);
2148 		if (outputState != null) {
2149 			outputState.sink.getPrintStream().close();
2150 		}
2151 		return outputState != null;
2152 	}
2153 
2154 	private boolean jrtCloseOutputStream(String cmd) {
2155 		IoState state = ioState;
2156 		if (state == null) {
2157 			return false;
2158 		}
2159 		ProcessOutputState outputState = state.processOutputs.remove(cmd);
2160 		if (outputState == null) {
2161 			return false;
2162 		}
2163 		outputState.sink.getPrintStream().close();
2164 		try {
2165 			// wait for the spawned process to finish to make sure
2166 			// all output has been flushed and captured
2167 			outputState.process.waitFor();
2168 			outputState.process.exitValue();
2169 		} catch (InterruptedException ie) {
2170 			Thread.currentThread().interrupt();
2171 			outputState.process.destroyForcibly();
2172 			throw new AwkRuntimeException(
2173 					"Caught exception while waiting for process exit: " + ie);
2174 		} finally {
2175 			joinDataPump(outputState.stdoutPump);
2176 			joinDataPump(outputState.stderrPump);
2177 			outputState.processOutput.flush();
2178 			error.flush();
2179 		}
2180 		return true;
2181 	}
2182 
2183 	private boolean jrtCloseFileReader(String fileNameParam) {
2184 		IoState state = ioState;
2185 		if (state == null) {
2186 			return false;
2187 		}
2188 		PartitioningReader pr = state.fileReaders.get(fileNameParam);
2189 		if (pr == null) {
2190 			return false;
2191 		}
2192 		state.fileReaders.remove(fileNameParam);
2193 		try {
2194 			pr.close();
2195 			return true;
2196 		} catch (IOException ioe) {
2197 			return false;
2198 		}
2199 	}
2200 
2201 	private boolean jrtCloseCommandReader(String cmd) {
2202 		IoState state = ioState;
2203 		if (state == null) {
2204 			return false;
2205 		}
2206 		CommandInputState commandInput = state.commandInputs.remove(cmd);
2207 		if (commandInput == null) {
2208 			return false;
2209 		}
2210 		try {
2211 			commandInput.reader.close();
2212 			try {
2213 				// wait for the process to complete so that all
2214 				// data pumped from the command is captured
2215 				commandInput.process.waitFor();
2216 				commandInput.process.exitValue();
2217 			} catch (InterruptedException ie) {
2218 				Thread.currentThread().interrupt();
2219 				commandInput.process.destroyForcibly();
2220 				throw new AwkRuntimeException(
2221 						"Caught exception while waiting for process exit: " + ie);
2222 			}
2223 			return true;
2224 		} catch (IOException ioe) {
2225 			return false;
2226 		} finally {
2227 			joinDataPump(commandInput.errorPump);
2228 			error.flush();
2229 		}
2230 	}
2231 
2232 	/**
2233 	 * Executes the command specified by cmd and waits
2234 	 * for termination, returning an Integer object
2235 	 * containing the return code.
2236 	 * stdin to this process is closed while
2237 	 * threads are created to shuttle stdout and
2238 	 * stderr of the command to stdout/stderr
2239 	 * of the calling process.
2240 	 *
2241 	 * @param cmd The command to execute.
2242 	 * @return Integer(return_code) of the created
2243 	 *         process. Integer(-1) is returned on an IO error.
2244 	 */
2245 	public Integer jrtSystem(String cmd) {
2246 		try {
2247 			PrintStream processOutput = awkSink.getPrintStream();
2248 			Process p = spawnProcess(cmd);
2249 			// no input to this process!
2250 			p.getOutputStream().close();
2251 			Thread errorPump = DataPump.dumpAndReturnThread(cmd + " stderr", p.getErrorStream(), error);
2252 			Thread outputPump = DataPump.dumpAndReturnThread(cmd + " stdout", p.getInputStream(), processOutput);
2253 			boolean interrupted = false;
2254 			int retcode;
2255 			while (true) {
2256 				try {
2257 					retcode = p.waitFor();
2258 					break;
2259 				} catch (InterruptedException ie) {
2260 					// Preserve interrupt and keep waiting so process pipes can close.
2261 					interrupted = true;
2262 				}
2263 			}
2264 			joinDataPump(outputPump);
2265 			joinDataPump(errorPump);
2266 			processOutput.flush();
2267 			error.flush();
2268 			if (interrupted) {
2269 				Thread.currentThread().interrupt();
2270 			}
2271 			return Integer.valueOf(retcode);
2272 		} catch (IOException ioe) {
2273 			return MINUS_ONE;
2274 		}
2275 	}
2276 
2277 	private static void joinDataPump(Thread pump) {
2278 		if (pump == null) {
2279 			return;
2280 		}
2281 		boolean interrupted = false;
2282 		while (true) {
2283 			try {
2284 				pump.join();
2285 				break;
2286 			} catch (InterruptedException ie) {
2287 				interrupted = true;
2288 			}
2289 		}
2290 		if (interrupted) {
2291 			Thread.currentThread().interrupt();
2292 		}
2293 	}
2294 
2295 	/**
2296 	 * <p>
2297 	 * sprintfFunctionNoCatch.
2298 	 * </p>
2299 	 *
2300 	 * @param locale a {@link java.util.Locale} object
2301 	 * @param fmtArg a {@link java.lang.String} object
2302 	 * @param arr an array of {@link java.lang.Object} objects
2303 	 * @return a {@link java.lang.String} object
2304 	 * @throws java.util.IllegalFormatException if any.
2305 	 */
2306 	public static String sprintfNoCatch(Locale locale, String fmtArg, Object... arr) throws IllegalFormatException {
2307 		return String.format(locale, fmtArg, arr);
2308 	}
2309 
2310 	/**
2311 	 * <p>
2312 	 * printfFunctionNoCatch.
2313 	 * </p>
2314 	 *
2315 	 * @param locale a {@link java.util.Locale} object
2316 	 * @param fmtArg a {@link java.lang.String} object
2317 	 * @param arr an array of {@link java.lang.Object} objects
2318 	 */
2319 	public static void printfNoCatch(Locale locale, String fmtArg, Object... arr) {
2320 		System.out.print(sprintfNoCatch(locale, fmtArg, arr));
2321 	}
2322 
2323 	/**
2324 	 * <p>
2325 	 * printfFunctionNoCatch.
2326 	 * </p>
2327 	 *
2328 	 * @param ps a {@link java.io.PrintStream} object
2329 	 * @param locale a {@link java.util.Locale} object
2330 	 * @param fmtArg a {@link java.lang.String} object
2331 	 * @param arr an array of {@link java.lang.Object} objects
2332 	 */
2333 	public static void printfNoCatch(PrintStream ps, Locale locale, String fmtArg, Object... arr) {
2334 		ps.print(sprintfNoCatch(locale, fmtArg, arr));
2335 	}
2336 
2337 	/**
2338 	 * <p>
2339 	 * substr.
2340 	 * </p>
2341 	 *
2342 	 * @param startposObj a {@link java.lang.Object} object
2343 	 * @param str a {@link java.lang.String} object
2344 	 * @return a {@link java.lang.String} object
2345 	 */
2346 	public static String substr(Object startposObj, String str) {
2347 		int startpos = (int) toDouble(startposObj);
2348 		if (startpos <= 0) {
2349 			throw new AwkRuntimeException("2nd arg to substr must be a positive integer");
2350 		}
2351 		if (startpos > str.length()) {
2352 			return "";
2353 		} else {
2354 			return str.substring(startpos - 1);
2355 		}
2356 	}
2357 
2358 	/**
2359 	 * <p>
2360 	 * substr.
2361 	 * </p>
2362 	 *
2363 	 * @param sizeObj a {@link java.lang.Object} object
2364 	 * @param startposObj a {@link java.lang.Object} object
2365 	 * @param str a {@link java.lang.String} object
2366 	 * @return a {@link java.lang.String} object
2367 	 */
2368 	public static String substr(Object sizeObj, Object startposObj, String str) {
2369 		int startpos = (int) toDouble(startposObj);
2370 		if (startpos <= 0) {
2371 			throw new AwkRuntimeException("2nd arg to substr must be a positive integer");
2372 		}
2373 		if (startpos > str.length()) {
2374 			return "";
2375 		}
2376 		int size = (int) toDouble(sizeObj);
2377 		if (size < 0) {
2378 			throw new AwkRuntimeException("3nd arg to substr must be a non-negative integer");
2379 		}
2380 		if (startpos + size > str.length()) {
2381 			return str.substring(startpos - 1);
2382 		} else {
2383 			return str.substring(startpos - 1, startpos + size - 1);
2384 		}
2385 	}
2386 
2387 	/**
2388 	 * <p>
2389 	 * timeSeed.
2390 	 * </p>
2391 	 *
2392 	 * @return a int
2393 	 */
2394 	public static int timeSeed() {
2395 		long l = new Date().getTime();
2396 		long l2 = l % (1000 * 60 * 60 * 24);
2397 		int seed = (int) l2;
2398 		return seed;
2399 	}
2400 
2401 	/**
2402 	 * <p>
2403 	 * newRandom.
2404 	 * </p>
2405 	 *
2406 	 * @param seed a int
2407 	 * @return a {@link java.util.Random} object
2408 	 */
2409 	public static BSDRandom newRandom(int seed) {
2410 		return new BSDRandom(seed);
2411 	}
2412 
2413 	/**
2414 	 * <p>
2415 	 * applyRS.
2416 	 * </p>
2417 	 *
2418 	 * @param rsObj a {@link java.lang.Object} object
2419 	 */
2420 	public void applyRS(Object rsObj) {
2421 		if (activeSource instanceof StreamInputSource) {
2422 			((StreamInputSource) activeSource).setRecordSeparator(rsObj.toString());
2423 		}
2424 	}
2425 }