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