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.File;
26  import java.io.FileOutputStream;
27  import java.io.FileInputStream;
28  import java.io.IOException;
29  import java.io.InputStream;
30  import java.io.InputStreamReader;
31  import java.io.PrintStream;
32  import java.nio.charset.StandardCharsets;
33  import java.text.DecimalFormatSymbols;
34  import java.util.ArrayList;
35  import java.util.Date;
36  import java.util.Enumeration;
37  import java.util.HashMap;
38  import java.util.IdentityHashMap;
39  import java.util.HashSet;
40  import java.util.IllegalFormatException;
41  import java.util.List;
42  import java.util.Locale;
43  import java.util.Map;
44  import java.util.Objects;
45  import java.util.Set;
46  import java.util.StringTokenizer;
47  import java.util.regex.Matcher;
48  import java.util.regex.Pattern;
49  import io.jawk.Awk;
50  import io.jawk.intermediate.UninitializedObject;
51  import io.jawk.intermediate.UntypedObject;
52  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
53  
54  /**
55   * The Jawk runtime coordinator.
56   * The JRT services interpreted and compiled Jawk scripts, mainly
57   * for IO and other non-CPU bound tasks. The goal is to house
58   * service functions into a Java-compiled class rather than
59   * to hand-craft service functions in byte-code, or cut-paste
60   * compiled JVM code into the compiled AWK script. Also,
61   * since these functions are non-CPU bound, the need for
62   * inlining is reduced.
63   * <p>
64   * Variable access is achieved through the VariableManager interface.
65   * The constructor requires a VariableManager instance (which, in
66   * this case, is the compiled Jawk class itself).
67   * <p>
68   * Main services include:
69   * <ul>
70   * <li>File and command output redirection via print(f).
71   * <li>File and command input redirection via getline.
72   * <li>Most built-in AWK functions, such as system(), sprintf(), etc.
73   * <li>Automatic AWK type conversion routines.
74   * <li>IO management for input rule processing.
75   * <li>Random number engine management.
76   * <li>Input field ($0, $1, ...) management.
77   * </ul>
78   * <p>
79   * All static and non-static service methods should be package-private
80   * to the resultant AWK script class rather than public. However,
81   * the resultant script class is not in the <code>io.jawk.jrt</code> package
82   * by default, and the user may reassign the resultant script class
83   * to another package. Therefore, all accessed methods are public.
84   *
85   * @see VariableManager
86   * @author Danny Daglas
87   */
88  public class JRT {
89  
90  	private static final boolean IS_WINDOWS = System.getProperty("os.name").indexOf("Windows") >= 0;
91  
92  	/**
93  	 * Ceiling for {@link #dynamicPatterns} and {@link #dynamicPatternsIgnoreCase}:
94  	 * scripts can synthesize an unbounded number of distinct dynamic regexps from
95  	 * input data, so a cache is dumped wholesale when it fills instead of growing
96  	 * without bound. Real scripts use a handful of dynamic regexps, so the limit
97  	 * is effectively never reached.
98  	 */
99  	private static final int DYNAMIC_PATTERN_CACHE_LIMIT = 256;
100 
101 	/** gawk special filename designating the standard input of the process. */
102 	private static final String DEV_STDIN = "/dev/stdin";
103 	/** gawk special filename designating the standard output of the process. */
104 	private static final String DEV_STDOUT = "/dev/stdout";
105 	/** gawk special filename designating the standard error of the process. */
106 	private static final String DEV_STDERR = "/dev/stderr";
107 	/** gawk special filename designating file descriptor 0 (standard input). */
108 	private static final String DEV_FD_0 = "/dev/fd/0";
109 	/** gawk special filename designating file descriptor 1 (standard output). */
110 	private static final String DEV_FD_1 = "/dev/fd/1";
111 	/** gawk special filename designating file descriptor 2 (standard error). */
112 	private static final String DEV_FD_2 = "/dev/fd/2";
113 	/** Filename designating the null device on every Unix system. */
114 	private static final String DEV_NULL = "/dev/null";
115 	/** Name of the null device on Windows. */
116 	private static final String WINDOWS_NULL_DEVICE = "NUL";
117 
118 	private final VariableManager vm;
119 
120 	private IoState ioState;
121 	/** Output sink used for plain AWK print/printf output. */
122 	private AwkSink awkSink;
123 	/** PrintStream used for command error output */
124 	private PrintStream error;
125 	/** PrintStream used for runtime warning messages, stderr by default. */
126 	private PrintStream warning = System.err;
127 	/**
128 	 * Stream backing the {@code /dev/stdin} special filename. It defaults to the
129 	 * standard input of the JVM and is replaced by the input stream the host
130 	 * configured for the run, so that {@code getline < "/dev/stdin"} reads the
131 	 * same data as the main input loop does when no operand is given.
132 	 */
133 	private InputStream standardInput = System.in;
134 
135 	private boolean spawnedProcessesInheritStandardInput;
136 	/**
137 	 * Sink writing to the standard error of the process, used by the
138 	 * {@code /dev/stderr} special filename; created on first use and discarded
139 	 * whenever the warning stream is replaced.
140 	 */
141 	private AwkSink standardErrorSink;
142 	/** Current IGNORECASE value, as assigned by the script or the host. */
143 	private Object ignorecase = Long.valueOf(0L);
144 	/** Precomputed truth of IGNORECASE, consulted by every regexp operation. */
145 	private boolean ignoreCase;
146 	/** Case-insensitive twins of precompiled patterns; created on first use. */
147 	private Map<Pattern, Pattern> caseInsensitivePatterns;
148 	/** Compiled case-sensitive dynamic (string) regexps, keyed by expression text; created on first use. */
149 	private Map<String, Pattern> dynamicPatterns;
150 	/** Compiled case-insensitive dynamic (string) regexps, keyed by expression text; created on first use. */
151 	private Map<String, Pattern> dynamicPatternsIgnoreCase;
152 	/** Reused buffer holding the result of the last sub()/gsub() replacement. */
153 	private final StringBuffer replaceResult = new StringBuffer();
154 	// Last input line consumed for getline-style transport.
155 	private Object inputLine = null;
156 	// Current record state ($0, $1, $2, ...).
157 	private RecordState recordState;
158 	// The currently active InputSource (set during consumeInput calls).
159 	private InputSource activeSource;
160 	private static final UninitializedObject BLANK = new UninitializedObject();
161 
162 	private static final Integer ONE = Integer.valueOf(1);
163 	private static final Integer ZERO = Integer.valueOf(0);
164 	private static final Integer MINUS_ONE = Integer.valueOf(-1);
165 	private String jrtInputString;
166 
167 	// JRT-managed special variables (runtime only)
168 	private long nr; // total record number
169 	private long fnr; // file record number
170 	private int rstart; // last match start (1-based)
171 	private int rlength; // last match length
172 	private Object filename; // current input filename scalar (or empty for stdin/pipe)
173 	private Object errno; // last input I/O error description (gawk ERRNO)
174 	private Object argind; // ARGV index of the current input file (gawk ARGIND)
175 	private boolean syntheticFilePresented; // custom InputSource already presented as a single "file"
176 	private String fs; // field separator
177 	private String rs; // record separator (regexp)
178 	private String ofs; // output field separator
179 	private String ors; // output record separator
180 	private String convfmt; // number-to-string format
181 	private String ofmt; // number-to-string for output
182 	private String subsep; // subscript separator
183 	private final Locale locale; // locale for number formatting
184 	private final char decimalSeparator; // locale decimal separator for strnum recognition
185 
186 	private static final class FileOutputState {
187 
188 		private final AwkSink sink;
189 
190 		private FileOutputState(AwkSink sinkParam) {
191 			this.sink = Objects.requireNonNull(sinkParam, "sink");
192 		}
193 	}
194 
195 	private static final class CommandInputState {
196 
197 		private final Process process;
198 		private final PartitioningReader reader;
199 		private final Thread errorPump;
200 
201 		private CommandInputState(Process processParam, PartitioningReader readerParam, Thread errorPumpParam) {
202 			this.process = Objects.requireNonNull(processParam, "process");
203 			this.reader = Objects.requireNonNull(readerParam, "reader");
204 			this.errorPump = errorPumpParam;
205 		}
206 	}
207 
208 	private static final class ProcessOutputState {
209 
210 		private final Process process;
211 		private final AwkSink sink;
212 		private final PrintStream processOutput;
213 		private final Thread stdoutPump;
214 		private final Thread stderrPump;
215 
216 		private ProcessOutputState(
217 				Process processParam,
218 				AwkSink sinkParam,
219 				PrintStream processOutputParam,
220 				Thread stdoutPumpParam,
221 				Thread stderrPumpParam) {
222 			this.process = Objects.requireNonNull(processParam, "process");
223 			this.sink = Objects.requireNonNull(sinkParam, "sink");
224 			this.processOutput = Objects.requireNonNull(processOutputParam, "processOutput");
225 			this.stdoutPump = stdoutPumpParam;
226 			this.stderrPump = stderrPumpParam;
227 		}
228 	}
229 
230 	/**
231 	 * Sink that flushes its stream after every operation, so that partial lines
232 	 * are visible immediately. Used by the {@code /dev/stderr} special filename,
233 	 * which gawk keeps unbuffered.
234 	 */
235 	private static final class FlushingAwkSink extends OutputStreamAwkSink {
236 
237 		private FlushingAwkSink(PrintStream printStream, Locale locale) {
238 			super(printStream, locale);
239 		}
240 
241 		@Override
242 		public void print(String ofs, String ors, String ofmt, Object... values) {
243 			super.print(ofs, ors, ofmt, values);
244 			flush();
245 		}
246 
247 		@Override
248 		public void printf(String ofs, String ors, String ofmt, String convfmt, String format, Object... values) {
249 			super.printf(ofs, ors, ofmt, convfmt, format, values);
250 			flush();
251 		}
252 	}
253 
254 	private static final class IoState {
255 
256 		private final Map<String, PartitioningReader> fileReaders = new HashMap<String, PartitioningReader>();
257 		private final Map<String, CommandInputState> commandInputs = new HashMap<String, CommandInputState>();
258 		private final Map<String, FileOutputState> fileOutputs = new HashMap<String, FileOutputState>();
259 		private final Map<String, ProcessOutputState> processOutputs = new HashMap<String, ProcessOutputState>();
260 		/**
261 		 * Sink handed out for each standard output special filename a redirection
262 		 * is currently open on. The sink is retained rather than resolved again on
263 		 * {@code close()}, so that the redirection is always flushed through the
264 		 * sink that actually received its writes, even if the runtime's default
265 		 * output sink has been replaced since.
266 		 */
267 		private final Map<String, AwkSink> specialOutputs = new HashMap<String, AwkSink>();
268 	}
269 
270 	/**
271 	 * Create a JRT with explicit default output and error streams.
272 	 *
273 	 * @param vm The VariableManager to use with this JRT.
274 	 * @param locale The Locale to use for number formatting.
275 	 * @param awkSink default output sink used by plain AWK print operations
276 	 * @param error default error stream used for process stderr
277 	 */
278 	@SuppressFBWarnings(value = {
279 			"EI_EXPOSE_REP2",
280 			"CT_CONSTRUCTOR_THROW" }, justification = "JRT must hold the provided runtime collaborators for later use;"
281 					+ " fail-fast argument validation with no security-sensitive state to protect from finalizer attacks")
282 	public JRT(VariableManager vm, Locale locale, AwkSink awkSink, PrintStream error) {
283 		this.vm = vm;
284 		this.locale = locale == null ? Locale.US : locale;
285 		this.decimalSeparator = DecimalFormatSymbols.getInstance(this.locale).getDecimalSeparator();
286 		this.awkSink = Objects.requireNonNull(awkSink, "awkSink");
287 		this.error = error == null ? System.err : error;
288 		this.nr = 0L;
289 		this.fnr = 0L;
290 		this.rstart = 0;
291 		this.rlength = 0;
292 		this.filename = "";
293 		this.fs = Awk.DEFAULT_FS;
294 		this.rs = Awk.DEFAULT_RS;
295 		this.ofs = Awk.DEFAULT_OFS;
296 		this.ors = Awk.DEFAULT_ORS;
297 		this.convfmt = Awk.DEFAULT_CONVFMT;
298 		this.ofmt = Awk.DEFAULT_OFMT;
299 		this.subsep = Awk.DEFAULT_SUBSEP;
300 	}
301 
302 	/**
303 	 * Sets the sink used by default {@code print} and {@code printf}
304 	 * operations.
305 	 *
306 	 * @param sink output sink to use
307 	 */
308 	public void setAwkSink(AwkSink sink) {
309 		awkSink = Objects.requireNonNull(sink, "awkSink");
310 	}
311 
312 	/**
313 	 * Sets the stream used for the stderr output of spawned processes
314 	 * (e.g.&nbsp;{@code system("...")}).
315 	 *
316 	 * @param errorStream stream to receive process stderr
317 	 */
318 	public void setErrorStream(PrintStream errorStream) {
319 		this.error = Objects.requireNonNull(errorStream, "errorStream");
320 	}
321 
322 	/**
323 	 * Sets the stream that receives runtime warning messages. Warnings default
324 	 * to {@link System#err}, mirroring where gawk sends its diagnostics, and are
325 	 * deliberately kept apart from the process-stderr stream so they can never
326 	 * leak into a captured script output.
327 	 *
328 	 * @param warningStream stream to receive runtime warnings
329 	 */
330 	public void setWarningStream(PrintStream warningStream) {
331 		this.warning = Objects.requireNonNull(warningStream, "warningStream");
332 		// The /dev/stderr sink wraps the warning stream: drop it so that the
333 		// next write is routed to the new stream.
334 		this.standardErrorSink = null;
335 	}
336 
337 	/**
338 	 * Binds the stream that the {@code /dev/stdin} special filename reads from to
339 	 * the input source of the execution that is starting. A stream-backed source
340 	 * lends the stream it falls back to when {@code ARGV} holds no filename, which
341 	 * is what the run treats as its standard input; a source that produces records
342 	 * some other way has no such stream, so {@code /dev/stdin} designates the
343 	 * standard input of the JVM.
344 	 *
345 	 * @param inputSource input source bound to this execution
346 	 */
347 	public void bindStandardInput(InputSource inputSource) {
348 		this.standardInput = inputSource instanceof StreamInputSource ?
349 				((StreamInputSource) inputSource).getDefaultInput() : System.in;
350 	}
351 
352 	/**
353 	 * Prints a runtime warning message to the warning stream (stderr by
354 	 * default), mirroring where gawk sends its diagnostics.
355 	 *
356 	 * @param message warning text to print
357 	 */
358 	public void printWarning(String message) {
359 		warning.println(message);
360 		warning.flush();
361 	}
362 
363 	/**
364 	 * Returns the default output sink used by {@code print} and {@code printf}.
365 	 *
366 	 * @return the current AWK sink
367 	 */
368 	public AwkSink getAwkSink() {
369 		return awkSink;
370 	}
371 
372 	/**
373 	 * Returns the locale used for number formatting in this runtime.
374 	 *
375 	 * @return the runtime locale
376 	 */
377 	public Locale getLocale() {
378 		return locale;
379 	}
380 
381 	private IoState getIoState() {
382 		if (ioState == null) {
383 			ioState = new IoState();
384 		}
385 		return ioState;
386 	}
387 
388 	/**
389 	 * Returns whether the supplied variable name is managed directly by JRT
390 	 * rather than through the AVM runtime stack.
391 	 *
392 	 * @param name variable name to inspect
393 	 * @return {@code true} when the variable is a JRT-managed special variable
394 	 */
395 	public static boolean isJrtManagedSpecialVariable(String name) {
396 		switch (name) {
397 		case "FS":
398 		case "RS":
399 		case "OFS":
400 		case "ORS":
401 		case "CONVFMT":
402 		case "OFMT":
403 		case "SUBSEP":
404 		case "FILENAME":
405 		case "NF":
406 		case "NR":
407 		case "FNR":
408 		case "ARGC":
409 		case "IGNORECASE":
410 		case "ERRNO":
411 		case "ARGIND":
412 			return true;
413 		default:
414 			return false;
415 		}
416 	}
417 
418 	/**
419 	 * Returns whether the name is a gawk-only special variable that POSIX
420 	 * mode treats as an ordinary identifier, like {@code gawk --posix} does.
421 	 * Shared by the parser and the interpreter so both stay in sync.
422 	 *
423 	 * @param name variable name to inspect
424 	 * @return {@code true} when POSIX mode must treat the name as ordinary
425 	 */
426 	public static boolean isGawkOnlySpecialVariable(String name) {
427 		return "ERRNO".equals(name) || "ARGIND".equals(name);
428 	}
429 
430 	/**
431 	 * Copies only the JRT-managed special variables from the supplied map.
432 	 *
433 	 * @param variableMap source variable map
434 	 * @return a new map containing only JRT-managed special variables
435 	 */
436 	public static Map<String, Object> copySpecialVariables(Map<String, Object> variableMap) {
437 		Map<String, Object> specialVariables = new HashMap<String, Object>();
438 		if (variableMap == null || variableMap.isEmpty()) {
439 			return specialVariables;
440 		}
441 		for (Map.Entry<String, Object> entry : variableMap.entrySet()) {
442 			if (isJrtManagedSpecialVariable(entry.getKey())) {
443 				specialVariables.put(entry.getKey(), entry.getValue());
444 			}
445 		}
446 		return specialVariables;
447 	}
448 
449 	/**
450 	 * Resets per-execution JRT state and re-applies the default runtime special
451 	 * variables for a new script or expression execution.
452 	 * <p>
453 	 * The {@code defaultFs} and {@code defaultRs} parameters allow the caller
454 	 * to configure the initial field and record separators. Other special variables
455 	 * ({@code OFS}, {@code ORS}, {@code CONVFMT}, {@code OFMT}, {@code SUBSEP})
456 	 * use their POSIX-mandated defaults (see {@link Awk} constants) which are
457 	 * platform-independent and therefore not parameterized. Platform-specific
458 	 * end-of-line handling is the responsibility of the {@link AwkSink}.
459 	 *
460 	 * @param defaultFs default field separator, or {@code null} for
461 	 *        {@link Awk#DEFAULT_FS}
462 	 * @param defaultRs default record separator
463 	 */
464 	public void prepareForExecution(String defaultFs, String defaultRs) {
465 		// Close any previously opened IO resources before resetting state.
466 		jrtCloseAll();
467 
468 		// Clear per-execution state (IO handles, counters, input state).
469 		ioState = null;
470 		inputLine = null;
471 		recordState = null;
472 		activeSource = null;
473 		jrtInputString = null;
474 		nr = 0L;
475 		fnr = 0L;
476 		rstart = 0;
477 		rlength = 0;
478 		filename = "";
479 		errno = "";
480 		argind = ZERO;
481 		syntheticFilePresented = false;
482 
483 		// Apply default runtime special variables.
484 		setFS(defaultFs == null ? Awk.DEFAULT_FS : defaultFs);
485 		setRS(defaultRs);
486 		setOFS(Awk.DEFAULT_OFS);
487 		setORS(Awk.DEFAULT_ORS);
488 		setCONVFMT(Awk.DEFAULT_CONVFMT);
489 		setOFMT(Awk.DEFAULT_OFMT);
490 		setSUBSEP(Awk.DEFAULT_SUBSEP);
491 		setFILENAMEViaJrt("");
492 		setNR(0);
493 		setFNR(0);
494 		setRSTART(0);
495 		setRLENGTH(0);
496 		setIGNORECASE(Long.valueOf(0L));
497 	}
498 
499 	/**
500 	 * Assign all -v variables.
501 	 *
502 	 * @param initialVarMap A map containing all initial variable
503 	 *        names and their values.
504 	 */
505 	public final void assignInitialVariables(Map<String, Object> initialVarMap) {
506 		for (Map.Entry<String, Object> var : initialVarMap.entrySet()) {
507 			String name = var.getKey();
508 			Object value = var.getValue();
509 			if (!applySpecialVariable(name, value)) {
510 				vm.assignVariable(name, value);
511 			}
512 		}
513 	}
514 
515 	/**
516 	 * Applies the assignment of a single JRT-managed special variable.
517 	 *
518 	 * @param name variable name
519 	 * @param value value to assign
520 	 * @return {@code true} when the name was a JRT-managed special variable,
521 	 *         {@code false} when the assignment was not handled
522 	 */
523 	public boolean applySpecialVariable(String name, Object value) {
524 		switch (name) {
525 		case "FS":
526 			setFS(value);
527 			return true;
528 		case "RS":
529 			setRS(value);
530 			return true;
531 		case "OFS":
532 			setOFS(value);
533 			return true;
534 		case "ORS":
535 			setORS(value);
536 			return true;
537 		case "CONVFMT":
538 			setCONVFMT(value);
539 			return true;
540 		case "OFMT":
541 			setOFMT(value);
542 			return true;
543 		case "SUBSEP":
544 			setSUBSEP(value);
545 			return true;
546 		case "FILENAME":
547 			setFILENAMEViaJrt(value);
548 			return true;
549 		case "NF":
550 			setNF(value);
551 			return true;
552 		case "NR":
553 			setNR(value);
554 			return true;
555 		case "FNR":
556 			setFNR(value);
557 			return true;
558 		case "ARGC":
559 			setARGC(value);
560 			return true;
561 		case "IGNORECASE":
562 			setIGNORECASE(value);
563 			return true;
564 		case "ERRNO":
565 			setERRNO(value);
566 			return true;
567 		case "ARGIND":
568 			setARGIND(value);
569 			return true;
570 		default:
571 			return false;
572 		}
573 	}
574 
575 	/**
576 	 * Applies only the JRT-managed special variable assignments from the
577 	 * supplied map (FS, RS, OFS, ORS, CONVFMT, OFMT, SUBSEP, FILENAME, NF,
578 	 * NR, FNR, ARGC, IGNORECASE). Non-special variables are silently skipped because
579 	 * they require the runtime stack to be fully initialized (which happens
580 	 * during tuple execution).
581 	 *
582 	 * @param variableMap a map of variable names to values
583 	 */
584 	public final void applySpecialVariables(Map<String, Object> variableMap) {
585 		if (variableMap == null || variableMap.isEmpty()) {
586 			return;
587 		}
588 		for (Map.Entry<String, Object> var : variableMap.entrySet()) {
589 			// Non-special variables are skipped; they are assigned later
590 			// via the tuple instruction stream
591 			applySpecialVariable(var.getKey(), var.getValue());
592 		}
593 	}
594 
595 	/**
596 	 * Called by AVM/compiled modules to assign local
597 	 * environment variables to an associative array
598 	 * (in this case, to ENVIRON).
599 	 *
600 	 * @param aa The associative array to populate with
601 	 *        environment variables. The module asserts that
602 	 *        the associative array is empty prior to population.
603 	 */
604 	public static void assignEnvironmentVariables(AssocArray aa) {
605 		Map<String, String> env = System.getenv();
606 		for (Map.Entry<String, String> var : env.entrySet()) {
607 			aa.put(var.getKey(), new StrNum(var.getValue()));
608 		}
609 	}
610 
611 	/**
612 	 * Creates an AWK-managed associative array and exposes it as a plain
613 	 * {@link Map} for callers that do not need the concrete runtime type.
614 	 *
615 	 * @param sortedArrayKeys {@code true} to keep keys sorted
616 	 * @return a new AWK associative array
617 	 */
618 	public static Map<Object, Object> createAwkMap(boolean sortedArrayKeys) {
619 		return AssocArray.create(sortedArrayKeys);
620 	}
621 
622 	/**
623 	 * Checks key existence using AWK semantics when the supplied map is backed by
624 	 * an {@link AssocArray}, otherwise falling back to regular {@link Map}
625 	 * semantics.
626 	 *
627 	 * @param map map to inspect
628 	 * @param key key to look up
629 	 * @return {@code true} when the key exists
630 	 */
631 	public static boolean containsAwkKey(Map<Object, Object> map, Object key) {
632 		if (map instanceof AssocArray) {
633 			return ((AssocArray) map).isIn(key);
634 		}
635 		return map.containsKey(key);
636 	}
637 
638 	/**
639 	 * Reads a map element using AWK semantics when the supplied map is backed by
640 	 * an {@link AssocArray}. For plain {@link Map} instances, missing or
641 	 * {@code null}-valued entries are exposed as the AWK blank value so later
642 	 * expression evaluation never receives a raw {@code null}.
643 	 *
644 	 * @param map map to inspect
645 	 * @param key key to look up
646 	 * @return the stored value, or the AWK blank value when no concrete value is
647 	 *         present
648 	 */
649 	public static Object getAssocArrayValue(Map<Object, Object> map, Object key) {
650 		if (map instanceof AssocArray) {
651 			return map.get(key);
652 		}
653 		Object value = map.get(key);
654 		return value != null ? value : BLANK;
655 	}
656 
657 	/**
658 	 * Returns the AWK string value of an associative array entry, or
659 	 * {@code null} when the array has no such key. This is the common way to
660 	 * read optional settings out of AWK arrays, such as
661 	 * {@code PROCINFO["sorted_in"]} or {@code ENVIRON["TZ"]}.
662 	 *
663 	 * @param map associative array to read
664 	 * @param key entry key
665 	 * @return the entry value converted with {@code CONVFMT}, or {@code null}
666 	 *         when the key is absent
667 	 */
668 	public String getAwkStringEntry(Map<Object, Object> map, Object key) {
669 		if (!containsAwkKey(map, key)) {
670 			return null;
671 		}
672 		return toAwkString(getAssocArrayValue(map, key));
673 	}
674 
675 	/**
676 	 * Convert Strings, Integers, and Doubles to Strings
677 	 * based on the CONVFMT variable contents and the stored Locale.
678 	 *
679 	 * @param o Object to convert.
680 	 * @return A String representation of o.
681 	 */
682 	public String toAwkString(Object o) {
683 		return AwkPrintf.toAwkString(o, this.convfmt, this.locale);
684 	}
685 
686 	/**
687 	 * Compares two objects with this runtime's {@code IGNORECASE}, {@code CONVFMT} and locale.
688 	 * <p>
689 	 * Prefer this over the static {@code compare2} overloads whenever a runtime is available: it is
690 	 * the only form that honours a {@code CONVFMT} assigned by the script.
691 	 *
692 	 * @param o1 The 1st object.
693 	 * @param o2 the 2nd object.
694 	 * @param mode the comparison mode, as in {@link #compare2(Object, Object, int)}
695 	 * @return a boolean
696 	 */
697 	public boolean compare(Object o1, Object o2, int mode) {
698 		return compare2(o1, o2, mode, isIgnoreCase(), this.convfmt, this.locale);
699 	}
700 
701 	/**
702 	 * Convert a String, Integer, or Double to Double.
703 	 *
704 	 * @param o Object to convert.
705 	 * @return the "double" value of o, or 0 if invalid
706 	 */
707 	public static double toDouble(final Object o) {
708 		if (o == null) {
709 			return 0;
710 		}
711 
712 		if (o instanceof Number) {
713 			return ((Number) o).doubleValue();
714 		}
715 
716 		if (o instanceof Character) {
717 			return (double) ((Character) o).charValue();
718 		}
719 
720 		if (o instanceof StrNum) {
721 			StrNum strNum = (StrNum) o;
722 			if (strNum.isNumber()) {
723 				return strNum.doubleValue();
724 			}
725 		}
726 
727 		// Convert the leading numeric prefix, as AWK does: "25fix" yields 25, and
728 		// text without a numeric prefix yields 0.
729 		String s = o.toString();
730 		int length = s.length();
731 		int start = 0;
732 		while (start < length && Character.isWhitespace(s.charAt(start))) {
733 			start++;
734 		}
735 		// AWK accepts an infinity or NaN only as a complete signed token, matched
736 		// without regard to case: "-inf" and "-INF" are -inf, while "-inform",
737 		// "-Infinity", and an unsigned "inf" are ordinary text.
738 		if (start < length) {
739 			char sign = s.charAt(start);
740 			if ((sign == '+' || sign == '-') && isBlankToEnd(s, start + 4)) {
741 				if (s.regionMatches(true, start + 1, "inf", 0, 3)) {
742 					return sign == '-' ? Double.NEGATIVE_INFINITY : Double.POSITIVE_INFINITY;
743 				}
744 				if (s.regionMatches(true, start + 1, "nan", 0, 3)) {
745 					return Double.NaN;
746 				}
747 			}
748 		}
749 		int end = numericPrefixEnd(s, start, '.');
750 		if (end == start) {
751 			return 0;
752 		}
753 		// Copying is only needed when the number is a strict prefix of the text.
754 		return Double.parseDouble(start == 0 && end == length ? s : s.substring(start, end));
755 	}
756 
757 	/**
758 	 * Determines whether a double value actually represents a long integer
759 	 * within the limits of floating point precision.
760 	 *
761 	 * @param d the double value to examine
762 	 * @return {@code true} if {@code d} is effectively an integer
763 	 */
764 	public static boolean isActuallyLong(double d) {
765 		double r = Math.rint(d);
766 		return Math.abs(d - r) < Math.ulp(d);
767 	}
768 
769 	/** 2^63 as a double: the first value beyond the signed 64-bit range. */
770 	private static final double TWO_POW_63 = 9.223372036854775808e18;
771 
772 	/**
773 	 * Converts a computed double to the canonical AWK scalar: a {@link Long}
774 	 * when the value is integral and representable as a signed 64-bit integer,
775 	 * and the {@link Double} itself otherwise. Values beyond the 64-bit range
776 	 * stay doubles so they are not silently saturated to
777 	 * {@link Long#MAX_VALUE}.
778 	 *
779 	 * @param d the computed value
780 	 * @return {@code d} as a {@link Long} when exactly representable, or as a
781 	 *         {@link Double}
782 	 */
783 	public static Object toScalarNumber(double d) {
784 		if (isActuallyLong(d)) {
785 			double rounded = Math.rint(d);
786 			if (rounded >= -TWO_POW_63 && rounded < TWO_POW_63) {
787 				return Long.valueOf((long) rounded);
788 			}
789 		}
790 		return Double.valueOf(d);
791 	}
792 
793 	/**
794 	 * Truncates a double toward zero, as AWK's {@code int()} does, returning a
795 	 * {@link Long} when the result is representable and a {@link Double}
796 	 * otherwise.
797 	 *
798 	 * @param d the value to truncate
799 	 * @return the truncated value as a canonical AWK scalar
800 	 */
801 	public static Object truncateToScalar(double d) {
802 		if (Double.isNaN(d) || Double.isInfinite(d)) {
803 			return Double.valueOf(d);
804 		}
805 		return toScalarNumber(d < 0 ? Math.ceil(d) : Math.floor(d));
806 	}
807 
808 	/**
809 	 * Returns whether a scalar is an exact 64-bit integer: a boxed
810 	 * {@link Long} or {@link Integer}, whose value is known without any
811 	 * floating-point rounding.
812 	 *
813 	 * @param o the scalar to examine
814 	 * @return {@code true} when {@code o} is a {@code Long} or an
815 	 *         {@code Integer}
816 	 */
817 	private static boolean isExactIntegral(Object o) {
818 		return o instanceof Long || o instanceof Integer;
819 	}
820 
821 	/**
822 	 * Adds two AWK scalars. When both operands are exact 64-bit integers and
823 	 * the sum fits in 64 bits, the result stays an exact {@link Long};
824 	 * otherwise both operands are converted with {@link #toDouble(Object)}
825 	 * and the result is a {@link Double}.
826 	 *
827 	 * @param o1 the left operand
828 	 * @param o2 the right operand
829 	 * @return {@code o1 + o2} as a canonical AWK scalar
830 	 */
831 	public static Object add(Object o1, Object o2) {
832 		if (isExactIntegral(o1) && isExactIntegral(o2)) {
833 			try {
834 				return Math.addExact(((Number) o1).longValue(), ((Number) o2).longValue());
835 			} catch (ArithmeticException overflow) {
836 				return toDouble(o1) + toDouble(o2);
837 			}
838 		}
839 		return toDouble(o1) + toDouble(o2);
840 	}
841 
842 	/**
843 	 * Subtracts two AWK scalars. When both operands are exact 64-bit integers
844 	 * and the difference fits in 64 bits, the result stays an exact
845 	 * {@link Long}; otherwise both operands are converted with
846 	 * {@link #toDouble(Object)} and the result is a {@link Double}.
847 	 *
848 	 * @param o1 the left operand
849 	 * @param o2 the right operand
850 	 * @return {@code o1 - o2} as a canonical AWK scalar
851 	 */
852 	public static Object subtract(Object o1, Object o2) {
853 		if (isExactIntegral(o1) && isExactIntegral(o2)) {
854 			try {
855 				return Math.subtractExact(((Number) o1).longValue(), ((Number) o2).longValue());
856 			} catch (ArithmeticException overflow) {
857 				return toDouble(o1) - toDouble(o2);
858 			}
859 		}
860 		return toDouble(o1) - toDouble(o2);
861 	}
862 
863 	/**
864 	 * Multiplies two AWK scalars. When both operands are exact 64-bit
865 	 * integers and the product fits in 64 bits, the result stays an exact
866 	 * {@link Long}; otherwise both operands are converted with
867 	 * {@link #toDouble(Object)} and the result is a {@link Double}.
868 	 *
869 	 * @param o1 the left operand
870 	 * @param o2 the right operand
871 	 * @return {@code o1 * o2} as a canonical AWK scalar
872 	 */
873 	public static Object multiply(Object o1, Object o2) {
874 		if (isExactIntegral(o1) && isExactIntegral(o2)) {
875 			try {
876 				return Math.multiplyExact(((Number) o1).longValue(), ((Number) o2).longValue());
877 			} catch (ArithmeticException overflow) {
878 				return toDouble(o1) * toDouble(o2);
879 			}
880 		}
881 		return toDouble(o1) * toDouble(o2);
882 	}
883 
884 	/**
885 	 * Divides two AWK scalars. When both operands are exact 64-bit integers
886 	 * and the quotient is a 64-bit integer with no remainder, the result
887 	 * stays an exact {@link Long}; every other case (a fractional quotient,
888 	 * a zero divisor, or {@code Long.MIN_VALUE / -1}) is computed in
889 	 * floating point, as before.
890 	 *
891 	 * @param o1 the dividend
892 	 * @param o2 the divisor
893 	 * @return {@code o1 / o2} as a canonical AWK scalar
894 	 */
895 	public static Object divide(Object o1, Object o2) {
896 		if (isExactIntegral(o1) && isExactIntegral(o2)) {
897 			long l1 = ((Number) o1).longValue();
898 			long l2 = ((Number) o2).longValue();
899 			if (l2 != 0 && l1 % l2 == 0 && (l1 != Long.MIN_VALUE || l2 != -1)) {
900 				return l1 / l2;
901 			}
902 		}
903 		return toDouble(o1) / toDouble(o2);
904 	}
905 
906 	/**
907 	 * Computes the remainder of two AWK scalars. When both operands are exact
908 	 * 64-bit integers and the divisor is non-zero, the result stays an exact
909 	 * {@link Long}; otherwise the remainder is computed in floating point,
910 	 * so a zero divisor still yields {@code nan}.
911 	 *
912 	 * @param o1 the dividend
913 	 * @param o2 the divisor
914 	 * @return {@code o1 % o2} as a canonical AWK scalar
915 	 */
916 	public static Object mod(Object o1, Object o2) {
917 		if (isExactIntegral(o1) && isExactIntegral(o2)) {
918 			long l2 = ((Number) o2).longValue();
919 			if (l2 != 0) {
920 				return ((Number) o1).longValue() % l2;
921 			}
922 		}
923 		return toDouble(o1) % toDouble(o2);
924 	}
925 
926 	/**
927 	 * Raises an AWK scalar to a power. Exponentiation is always computed in
928 	 * floating point, like gawk's {@code ^} operator.
929 	 *
930 	 * @param o1 the base
931 	 * @param o2 the exponent
932 	 * @return {@code o1 ^ o2} as a {@link Double}
933 	 */
934 	public static Object pow(Object o1, Object o2) {
935 		return Math.pow(toDouble(o1), toDouble(o2));
936 	}
937 
938 	/**
939 	 * Negates an AWK scalar. An exact 64-bit integer stays an exact
940 	 * {@link Long} (except {@code Long.MIN_VALUE}, whose negation does not
941 	 * fit); everything else is converted with {@link #toDouble(Object)} and
942 	 * negated as a {@link Double}.
943 	 *
944 	 * @param o the scalar to negate
945 	 * @return {@code -o} as a canonical AWK scalar
946 	 */
947 	public static Object negate(Object o) {
948 		if (isExactIntegral(o)) {
949 			long l = ((Number) o).longValue();
950 			if (l != Long.MIN_VALUE) {
951 				return -l;
952 			}
953 		}
954 		return -toDouble(o);
955 	}
956 
957 	/**
958 	 * Convert a String, Long, or Double to Long.
959 	 *
960 	 * @param o Object to convert.
961 	 * @return the "long" value of o, or 0 if invalid
962 	 */
963 	public static long toLong(final Object o) {
964 		if (o == null) {
965 			return 0;
966 		}
967 
968 		if (o instanceof Number) {
969 			return ((Number) o).longValue();
970 		}
971 
972 		if (o instanceof Character) {
973 			return (long) ((Character) o).charValue();
974 		}
975 
976 		// Whole numbers, by far the most common case here, are accumulated
977 		// directly so that every value a long can hold converts exactly.
978 		// Anything else -- a fractional part, an exponent, or a value beyond
979 		// the 64-bit range -- goes through the same conversion as everywhere
980 		// else and truncates toward zero, so that "1e1" converts to 10 rather
981 		// than 1. Values beyond the 64-bit range saturate.
982 		String s = o.toString();
983 		int length = s.length();
984 		int index = 0;
985 		while (index < length && Character.isWhitespace(s.charAt(index))) {
986 			index++;
987 		}
988 		boolean negative = index < length && s.charAt(index) == '-';
989 		if (index < length && (negative || s.charAt(index) == '+')) {
990 			index++;
991 		}
992 		int firstDigit = index;
993 		long value = 0;
994 		while (index < length && isAsciiDigit(s.charAt(index))) {
995 			int digit = s.charAt(index) - '0';
996 			if (value > (Long.MAX_VALUE - digit) / 10) {
997 				// Stop before overflowing: the conversion below then takes over
998 				// and saturates, since a digit is necessarily left unconsumed.
999 				break;
1000 			}
1001 			value = value * 10 + digit;
1002 			index++;
1003 		}
1004 		if (index > firstDigit && !continuesNumber(s, index)) {
1005 			return negative ? -value : value;
1006 		}
1007 		return (long) toDouble(o);
1008 	}
1009 
1010 	/**
1011 	 * Returns whether the text holds nothing but whitespace from {@code index}
1012 	 * onwards, treating an index past the end as satisfied.
1013 	 */
1014 	private static boolean isBlankToEnd(String value, int index) {
1015 		for (int i = index; i < value.length(); i++) {
1016 			if (!Character.isWhitespace(value.charAt(i))) {
1017 				return false;
1018 			}
1019 		}
1020 		return true;
1021 	}
1022 
1023 	/**
1024 	 * Returns whether the text at {@code index} extends a run of digits into a
1025 	 * number that no longer converts to the same integer. An exponent marker
1026 	 * only does so when digits actually follow it, matching the backtracking in
1027 	 * {@link #numericPrefixEnd(String, int, char)}: the numeric prefix of
1028 	 * {@code "12e"} is just {@code "12"}, which is exactly the integer already
1029 	 * accumulated.
1030 	 */
1031 	private static boolean continuesNumber(String value, int index) {
1032 		if (index >= value.length()) {
1033 			return false;
1034 		}
1035 		char c = value.charAt(index);
1036 		if (isAsciiDigit(c) || c == '.') {
1037 			return true;
1038 		}
1039 		if (c != 'e' && c != 'E') {
1040 			return false;
1041 		}
1042 		int afterExponent = index + 1;
1043 		if (afterExponent < value.length()
1044 				&& (value.charAt(afterExponent) == '+' || value.charAt(afterExponent) == '-')) {
1045 			afterExponent++;
1046 		}
1047 		return afterExponent < value.length() && isAsciiDigit(value.charAt(afterExponent));
1048 	}
1049 
1050 	/**
1051 	 * Convert a field designator to a non-negative long, raising an AWK runtime
1052 	 * exception when the value is invalid.
1053 	 *
1054 	 * @param obj the object identifying the field (for example, the result of a
1055 	 *        numeric expression)
1056 	 * @return the parsed field number as a long
1057 	 */
1058 	public static long parseFieldNumber(Object obj) {
1059 		long num = toLong(obj);
1060 		if (num < 0) {
1061 			throw new AwkRuntimeException(
1062 					"Field $(" + obj.toString()
1063 							+ ") is incorrect.");
1064 		}
1065 		return num;
1066 	}
1067 
1068 	/**
1069 	 * Compares two objects. Whether to employ less-than, equals, or
1070 	 * greater-than checks depends on the mode chosen by the callee.
1071 	 * It handles Awk variable rules and type conversion semantics.
1072 	 *
1073 	 * @param o1 The 1st object.
1074 	 * @param o2 the 2nd object.
1075 	 * @param mode
1076 	 *        <ul>
1077 	 *        <li>&lt; 0 - Return true if o1 &lt; o2.
1078 	 *        <li>0 - Return true if o1 == o2.
1079 	 *        <li>&gt; 0 - Return true if o1 &gt; o2.
1080 	 *        </ul>
1081 	 * @return a boolean
1082 	 */
1083 	public static boolean compare2(Object o1, Object o2, int mode) {
1084 		return compare2(o1, o2, mode, false);
1085 	}
1086 
1087 	/**
1088 	 * Compares two objects like {@link #compare2(Object, Object, int)}, folding
1089 	 * case in string comparisons when {@code ignoreCase} is set: gawk's
1090 	 * {@code IGNORECASE} applies to string relational operators, not only to
1091 	 * regexp operations.
1092 	 *
1093 	 * @param o1 The 1st object.
1094 	 * @param o2 the 2nd object.
1095 	 * @param mode the comparison mode, as in {@link #compare2(Object, Object, int)}
1096 	 * @param ignoreCase whether string comparisons ignore case
1097 	 * @return a boolean
1098 	 */
1099 	public static boolean compare2(Object o1, Object o2, int mode, boolean ignoreCase) {
1100 		// The default CONVFMT is the only one available here. It is also the only correct choice for
1101 		// the caller this form exists for, AwkTuples' constant folding, which runs at compile time
1102 		// when no runtime and therefore no script-assigned CONVFMT exists yet. Anything holding a
1103 		// runtime must call compare(Object, Object, int) instead.
1104 		return compare2(o1, o2, mode, ignoreCase, null, Locale.US);
1105 	}
1106 
1107 	/**
1108 	 * Shared implementation, taking every property the comparison depends on explicitly.
1109 	 * <p>
1110 	 * A number compared against a string is a string comparison in AWK, and the number must be
1111 	 * converted with the AWK number-to-string rule: a value exactly equal to an integer renders as
1112 	 * that integer, anything else through {@code CONVFMT}. Using {@link Object#toString()} here would
1113 	 * render {@code 291} as {@code "291.0"} and {@code 1.04152956928E11} as {@code "1.04152956928E11"},
1114 	 * so a computed integer would stop comparing equal to its own digits.
1115 	 * <p>
1116 	 * Deliberately not public: {@link #compare(Object, Object, int)} is the form to use, and it reads
1117 	 * these properties off the runtime. The static entry points remain only for the callers that have
1118 	 * no runtime to read them from.
1119 	 *
1120 	 * @param o1 The 1st object.
1121 	 * @param o2 the 2nd object.
1122 	 * @param mode the comparison mode, as in {@link #compare2(Object, Object, int)}
1123 	 * @param ignoreCase whether string comparisons ignore case
1124 	 * @param convfmt the {@code CONVFMT} to apply, or {@code null} for the default
1125 	 * @param locale the locale used to format numbers
1126 	 * @return a boolean
1127 	 */
1128 	private static boolean compare2(
1129 			Object o1,
1130 			Object o2,
1131 			int mode,
1132 			boolean ignoreCase,
1133 			String convfmt,
1134 			Locale locale) {
1135 		if (o1 instanceof Number && o2 instanceof Number) {
1136 			if (isExactIntegral(o1) && isExactIntegral(o2)) {
1137 				// Compare exact 64-bit integers without the precision loss a
1138 				// double conversion would introduce beyond 2^53.
1139 				int comparison = Long.compare(((Number) o1).longValue(), ((Number) o2).longValue());
1140 				if (mode == 0) {
1141 					return comparison == 0;
1142 				}
1143 				return mode < 0 ? comparison < 0 : comparison > 0;
1144 			}
1145 			return compareNumbers(((Number) o1).doubleValue(), ((Number) o2).doubleValue(), mode);
1146 		}
1147 
1148 		if (o1 instanceof UninitializedObject) {
1149 			if (isBlankOrZero(o2, convfmt, locale)) {
1150 				return mode == 0;
1151 			} else {
1152 				return mode < 0;
1153 			}
1154 		}
1155 		if (o2 instanceof UninitializedObject) {
1156 			if (isBlankOrZero(o1, convfmt, locale)) {
1157 				return mode == 0;
1158 			} else {
1159 				return mode > 0;
1160 			}
1161 		}
1162 
1163 		if (isNumericComparisonOperand(o1) && isNumericComparisonOperand(o2)) {
1164 			return compareNumbers(getDoubleForComparison(o1), getDoubleForComparison(o2), mode);
1165 		}
1166 
1167 		// Only a genuine string comparison converts, and only here. CONVFMT must not be evaluated for a
1168 		// comparison that turns out to be numeric: besides the discarded string, a format such as
1169 		// "%f%f" would otherwise fail on a comparison that has no string operand at all.
1170 		String o1String = AwkPrintf.toAwkString(o1, convfmt, locale);
1171 		String o2String = AwkPrintf.toAwkString(o2, convfmt, locale);
1172 
1173 		if (mode == 0) {
1174 			return ignoreCase ? o1String.equalsIgnoreCase(o2String) : o1String.equals(o2String);
1175 		}
1176 		int comparison = ignoreCase ? o1String.compareToIgnoreCase(o2String) : o1String.compareTo(o2String);
1177 		return mode < 0 ? comparison < 0 : comparison > 0;
1178 	}
1179 
1180 	/**
1181 	 * Implements the {@code index()} builtin: the 1-based position of
1182 	 * {@code needle} within {@code haystack}, or 0 when absent, folding case
1183 	 * when {@code IGNORECASE} is set.
1184 	 *
1185 	 * @param haystack text to search
1186 	 * @param needle text to find
1187 	 * @return 1-based match position, 0 when not found
1188 	 */
1189 	public int index(String haystack, String needle) {
1190 		if (!ignoreCase) {
1191 			return haystack.indexOf(needle) + 1;
1192 		}
1193 		int max = haystack.length() - needle.length();
1194 		for (int i = 0; i <= max; i++) {
1195 			if (haystack.regionMatches(true, i, needle, 0, needle.length())) {
1196 				return i + 1;
1197 			}
1198 		}
1199 		return 0;
1200 	}
1201 
1202 	/**
1203 	 * Whether the value counts as blank or zero when compared against an uninitialized value.
1204 	 * <p>
1205 	 * The string form is produced only for a value that is neither uninitialized nor numeric, so a
1206 	 * numeric operand never evaluates {@code CONVFMT} here.
1207 	 *
1208 	 * @param value the value to test
1209 	 * @param convfmt the {@code CONVFMT} to apply, or {@code null} for the default
1210 	 * @param locale the locale used to format numbers
1211 	 * @return whether the value is blank or zero
1212 	 */
1213 	private static boolean isBlankOrZero(Object value, String convfmt, Locale locale) {
1214 		if (value instanceof UninitializedObject) {
1215 			return true;
1216 		}
1217 		if (value instanceof Number) {
1218 			return ((Number) value).doubleValue() == 0.0D;
1219 		}
1220 		if (value instanceof StrNum && ((StrNum) value).isNumber()) {
1221 			return ((StrNum) value).doubleValue() == 0.0D;
1222 		}
1223 		String stringValue = AwkPrintf.toAwkString(value, convfmt, locale);
1224 		return "".equals(stringValue) || "0".equals(stringValue);
1225 	}
1226 
1227 	private static boolean isNumericComparisonOperand(Object value) {
1228 		return value instanceof Number || value instanceof StrNum && ((StrNum) value).isNumber();
1229 	}
1230 
1231 	private static double getDoubleForComparison(Object value) {
1232 		if (value instanceof Number) {
1233 			return ((Number) value).doubleValue();
1234 		}
1235 		return ((StrNum) value).doubleValue();
1236 	}
1237 
1238 	private static boolean compareNumbers(double o1Number, double o2Number, int mode) {
1239 		if (mode < 0) {
1240 			return o1Number < o2Number;
1241 		} else if (mode == 0) {
1242 			return o1Number == o2Number;
1243 		} else {
1244 			return o1Number > o2Number;
1245 		}
1246 	}
1247 
1248 	/**
1249 	 * Converts an internal runtime scalar to the value exposed through Java APIs.
1250 	 *
1251 	 * @param value internal scalar value
1252 	 * @return plain Java scalar value
1253 	 */
1254 	public static Object toJavaScalar(Object value) {
1255 		if (value instanceof StrNum) {
1256 			return value.toString();
1257 		}
1258 		if (value instanceof Double || value instanceof Float) {
1259 			return toScalarNumber(((Number) value).doubleValue());
1260 		}
1261 		return value;
1262 	}
1263 
1264 	/**
1265 	 * Returns whether the supplied text parses as an AWK number under this
1266 	 * runtime's locale, as used for strnum recognition. As POSIX specifies for
1267 	 * numeric strings, leading and trailing blanks around the number are
1268 	 * ignored, but text that is nothing but blanks does not qualify.
1269 	 *
1270 	 * @param value text to test
1271 	 * @return {@code true} when {@code value} is an input numeric string
1272 	 */
1273 	public boolean isParseableNumber(String value) {
1274 		return isParseableNumber(value, decimalSeparator);
1275 	}
1276 
1277 	/**
1278 	 * Replaces the untyped marker by AWK's assigned blank scalar. Reading a
1279 	 * missing array element creates and returns the untyped marker (so
1280 	 * {@code typeof()} can see it), but an assignment must not propagate it:
1281 	 * after {@code x = a[missing]}, {@code x} is an assigned blank scalar
1282 	 * ({@code typeof(x) == "unassigned"}), exactly as in gawk. This is a single
1283 	 * {@code instanceof} on the assignment paths.
1284 	 *
1285 	 * @param value value about to be stored by an assignment
1286 	 * @return the assigned blank scalar when the value was the untyped marker,
1287 	 *         otherwise the original value
1288 	 */
1289 	public static Object untypedToBlank(Object value) {
1290 		return value instanceof UntypedObject ? BLANK : value;
1291 	}
1292 
1293 	static boolean isParseableNumber(String value, char decimalSeparator) {
1294 		int length = value.length();
1295 		int start = 0;
1296 		while (start < length && Character.isWhitespace(value.charAt(start))) {
1297 			start++;
1298 		}
1299 		int end = numericPrefixEnd(value, start, decimalSeparator);
1300 		return end > start && isBlankToEnd(value, end);
1301 	}
1302 
1303 	/**
1304 	 * Strips the leading and trailing whitespace that strnum recognition
1305 	 * ignores, so a recognized numeric string can be handed to the Java
1306 	 * numeric parsers, which do not accept every character
1307 	 * {@link Character#isWhitespace(char)} does.
1308 	 *
1309 	 * @param value text to trim
1310 	 * @return {@code value} without leading and trailing whitespace
1311 	 */
1312 	static String trimWhitespace(String value) {
1313 		int end = value.length();
1314 		int start = 0;
1315 		while (start < end && Character.isWhitespace(value.charAt(start))) {
1316 			start++;
1317 		}
1318 		while (end > start && Character.isWhitespace(value.charAt(end - 1))) {
1319 			end--;
1320 		}
1321 		return start == 0 && end == value.length() ? value : value.substring(start, end);
1322 	}
1323 
1324 	/**
1325 	 * Scans the AWK numeric constant that starts at {@code start} and returns the
1326 	 * index just past it, or {@code start} when no number starts there. The
1327 	 * grammar is an optional sign, decimal digits with an optional fractional
1328 	 * part, and an optional exponent.
1329 	 * <p>
1330 	 * The scan is greedy but backtracks over an exponent marker that is not
1331 	 * followed by digits, so {@code "1e"} and {@code "1e+"} yield the prefix
1332 	 * {@code "1"} rather than failing, matching how AWK converts a string with a
1333 	 * numeric prefix. The caller is responsible for skipping any leading
1334 	 * whitespace it wants to allow.
1335 	 * </p>
1336 	 *
1337 	 * @param value text to scan
1338 	 * @param start index at which the number is expected to start
1339 	 * @param decimalSeparator character separating the integral and fractional
1340 	 *        parts under the active locale
1341 	 * @return the index just past the numeric prefix, or {@code start} when
1342 	 *         {@code value} has no numeric prefix at {@code start}
1343 	 */
1344 	private static int numericPrefixEnd(String value, int start, char decimalSeparator) {
1345 		int length = value.length();
1346 		int index = start;
1347 
1348 		if (index < length && (value.charAt(index) == '+' || value.charAt(index) == '-')) {
1349 			index++;
1350 		}
1351 
1352 		boolean digitFound = false;
1353 		while (index < length && isAsciiDigit(value.charAt(index))) {
1354 			index++;
1355 			digitFound = true;
1356 		}
1357 
1358 		if (index < length && value.charAt(index) == decimalSeparator) {
1359 			index++;
1360 			while (index < length && isAsciiDigit(value.charAt(index))) {
1361 				index++;
1362 				digitFound = true;
1363 			}
1364 		}
1365 
1366 		if (!digitFound) {
1367 			return start;
1368 		}
1369 
1370 		if (index < length && (value.charAt(index) == 'e' || value.charAt(index) == 'E')) {
1371 			int afterExponent = index + 1;
1372 			if (afterExponent < length
1373 					&& (value.charAt(afterExponent) == '+' || value.charAt(afterExponent) == '-')) {
1374 				afterExponent++;
1375 			}
1376 			int exponentDigits = afterExponent;
1377 			while (afterExponent < length && isAsciiDigit(value.charAt(afterExponent))) {
1378 				afterExponent++;
1379 			}
1380 			// Keep the exponent only when it has at least one digit: "1e" is the
1381 			// number 1 followed by text, not a malformed number.
1382 			if (afterExponent > exponentDigits) {
1383 				index = afterExponent;
1384 			}
1385 		}
1386 
1387 		return index;
1388 	}
1389 
1390 	private static boolean isAsciiDigit(char c) {
1391 		return c >= '0' && c <= '9';
1392 	}
1393 
1394 	static String normalizeNumberForComparison(String value, char decimalSeparator) {
1395 		return decimalSeparator == '.' ? value : value.replace(decimalSeparator, '.');
1396 	}
1397 
1398 	/**
1399 	 * Return an object which is numerically equivalent to
1400 	 * one plus a given object. An exact 64-bit integer stays an exact
1401 	 * {@link Long} (unless the increment overflows). For other numbers and
1402 	 * for Strings, the value is converted to a double first; a String
1403 	 * without a numeric prefix counts as 0, so the result is 1.
1404 	 *
1405 	 * @param o The object to increase.
1406 	 * @return {@code o + 1} if o is numeric or contains a numeric prefix;
1407 	 *         otherwise, {@code 1.0}
1408 	 */
1409 	public static Object inc(Object o) {
1410 		if (isExactIntegral(o)) {
1411 			long l = ((Number) o).longValue();
1412 			if (l != Long.MAX_VALUE) {
1413 				return l + 1;
1414 			}
1415 		}
1416 		return toDouble(o) + 1;
1417 	}
1418 
1419 	/**
1420 	 * Return an object which is numerically equivalent to
1421 	 * one minus a given object. An exact 64-bit integer stays an exact
1422 	 * {@link Long} (unless the decrement overflows). For other numbers and
1423 	 * for Strings, the value is converted to a double first; a String
1424 	 * without a numeric prefix counts as 0, so the result is -1.
1425 	 *
1426 	 * @param o The object to increase.
1427 	 * @return {@code o - 1} if o is numeric or contains a numeric prefix;
1428 	 *         otherwise, {@code -1.0}
1429 	 */
1430 	public static Object dec(Object o) {
1431 		if (isExactIntegral(o)) {
1432 			long l = ((Number) o).longValue();
1433 			if (l != Long.MIN_VALUE) {
1434 				return l - 1;
1435 			}
1436 		}
1437 		return toDouble(o) - 1;
1438 	}
1439 
1440 	// non-static to reference "inputLine"
1441 	/**
1442 	 * Converts an Integer, Double, String, Pattern,
1443 	 * or ConditionPair to a boolean.
1444 	 *
1445 	 * @param o The object to convert to a boolean.
1446 	 * @return For the following class types for o:
1447 	 *         <ul>
1448 	 *         <li><strong>Integer</strong> - o.intValue() != 0
1449 	 *         <li><strong>Long</strong> - o.longValue() != 0
1450 	 *         <li><strong>Double</strong> - o.doubleValue() != 0
1451 	 *         <li><strong>String</strong> - o.length() &gt; 0
1452 	 *         <li><strong>UninitializedObject</strong> - false
1453 	 *         <li><strong>Pattern</strong> - $0 ~ o
1454 	 *         </ul>
1455 	 *         If o is none of these types, an error is thrown.
1456 	 */
1457 	public final boolean toBoolean(Object o) {
1458 		boolean val;
1459 		if (o instanceof Integer) {
1460 			val = ((Integer) o).intValue() != 0;
1461 		} else if (o instanceof Long) {
1462 			val = ((Long) o).longValue() != 0;
1463 		} else if (o instanceof Double) {
1464 			val = ((Double) o).doubleValue() != 0;
1465 		} else if (o instanceof StrNum) {
1466 			StrNum strNum = (StrNum) o;
1467 			val = strNum.isNumber() ? strNum.doubleValue() != 0 : strNum.toString().length() > 0;
1468 		} else if (o instanceof String) {
1469 			val = (o.toString().length() > 0);
1470 		} else if (o instanceof UninitializedObject) {
1471 			val = false;
1472 		} else if (o instanceof Pattern) {
1473 			// match against $0
1474 			Pattern pattern = caseAwarePattern((Pattern) o);
1475 			Object inputField = jrtGetInputField(0);
1476 			String s = inputField instanceof UninitializedObject ? "" : inputField.toString();
1477 			Matcher matcher = pattern.matcher(s);
1478 			val = matcher.find();
1479 		} else {
1480 			throw new Error("Unknown operand_stack type: " + o.getClass() + " for value " + o);
1481 		}
1482 		return val;
1483 	}
1484 
1485 	/**
1486 	 * Splits the string into parts separated by one or more spaces;
1487 	 * blank first and last fields are eliminated.
1488 	 * This conforms to the 2-argument version of AWK's split function.
1489 	 *
1490 	 * @param array The array to populate.
1491 	 * @param string The string to split.
1492 	 * @return The number of parts resulting from this split operation.
1493 	 */
1494 	public int split(Object array, Object string) {
1495 		return splitWorker(new StringTokenizer(toAwkString(string)), toArrayMap(array));
1496 	}
1497 
1498 	/**
1499 	 * Splits the string into parts separated the regular expression fs.
1500 	 * This conforms to the 3-argument version of AWK's split function.
1501 	 * <p>
1502 	 * If fs is blank, it behaves similar to the 2-arg version of
1503 	 * AWK's split function.
1504 	 *
1505 	 * @param fieldSeparator Field separator regular expression.
1506 	 * @param array The array to populate.
1507 	 * @param string The string to split.
1508 	 * @return The number of parts resulting from this split operation.
1509 	 */
1510 	public int split(Object fieldSeparator, Object array, Object string) {
1511 		return splitWorker(splitTokenizer(toAwkString(string), fieldSeparator), toArrayMap(array));
1512 	}
1513 
1514 	private static Map<Object, Object> toArrayMap(Object array) {
1515 		if (!(array instanceof Map)) {
1516 			throw new IllegalArgumentException("split target must be a Map.");
1517 		}
1518 		@SuppressWarnings("unchecked")
1519 		Map<Object, Object> arrayMap = (Map<Object, Object>) array;
1520 		return arrayMap;
1521 	}
1522 
1523 	private int splitWorker(Enumeration<Object> e, Map<Object, Object> array) {
1524 		int cnt = 0;
1525 		array.clear();
1526 		while (e.hasMoreElements()) {
1527 			Object value = e.nextElement();
1528 			array.put(Long.valueOf(++cnt), toInputScalar(value));
1529 		}
1530 		array.put(0L, Long.valueOf(cnt));
1531 		return cnt;
1532 	}
1533 
1534 	/**
1535 	 * Returns the underlying {@link PartitioningReader} currently in use by
1536 	 * the active {@link InputSource}, or {@code null} if the source is not
1537 	 * stream-based.
1538 	 *
1539 	 * @return the active reader, or {@code null}
1540 	 */
1541 	public PartitioningReader getPartitioningReader() {
1542 		if (activeSource instanceof StreamInputSource) {
1543 			return ((StreamInputSource) activeSource).getPartitioningReader();
1544 		}
1545 		return null;
1546 	}
1547 
1548 	/**
1549 	 * <p>
1550 	 * Getter for the field <code>inputLine</code>.
1551 	 * </p>
1552 	 *
1553 	 * @return the current input line scalar value, or {@code null}
1554 	 */
1555 	public Object getInputLine() {
1556 		if (recordState != null) {
1557 			return recordState.getField(0);
1558 		}
1559 		return inputLine;
1560 	}
1561 
1562 	/**
1563 	 * Retrieve the current value of NF. When fields are initialized this returns
1564 	 * the number of fields in $0; otherwise 0.
1565 	 *
1566 	 * @return current NF value
1567 	 */
1568 	public Integer getNF() {
1569 		if (recordState == null) {
1570 			return Integer.valueOf(0);
1571 		}
1572 		return Integer.valueOf(recordState.getNF());
1573 	}
1574 
1575 	/**
1576 	 * Set NF to the specified value and update $0 and fields accordingly.
1577 	 *
1578 	 * @param nfObject value to assign to NF
1579 	 */
1580 	public void setNF(Object nfObject) {
1581 		jrtSetNF(nfObject);
1582 	}
1583 
1584 	/**
1585 	 * Get the current NR value as tracked by JRT.
1586 	 *
1587 	 * @return current NR
1588 	 */
1589 	public Long getNR() {
1590 		return Long.valueOf(nr);
1591 	}
1592 
1593 	/**
1594 	 * Assign NR to a specific value; also updates the VariableManager copy.
1595 	 *
1596 	 * @param value value to assign
1597 	 */
1598 	public void setNR(Object value) {
1599 		this.nr = toLong(value);
1600 	}
1601 
1602 	/**
1603 	 * Get the current FNR value as tracked by JRT.
1604 	 *
1605 	 * @return current FNR
1606 	 */
1607 	public Long getFNR() {
1608 		return Long.valueOf(fnr);
1609 	}
1610 
1611 	/**
1612 	 * Assign FNR to a specific value; also updates the VariableManager copy.
1613 	 *
1614 	 * @param value value to assign
1615 	 */
1616 	public void setFNR(Object value) {
1617 		this.fnr = toLong(value);
1618 	}
1619 
1620 	/**
1621 	 * Get FS from the VariableManager.
1622 	 *
1623 	 * @return FS value
1624 	 */
1625 	public Object getFSVar() {
1626 		return fs;
1627 	}
1628 
1629 	/**
1630 	 * Returns the current FS value as a string.
1631 	 *
1632 	 * @return current field separator
1633 	 */
1634 	public String getFSString() {
1635 		return fs;
1636 	}
1637 
1638 	/**
1639 	 * Set FS via the VariableManager.
1640 	 *
1641 	 * @param value new FS value
1642 	 */
1643 	public void setFS(Object value) {
1644 		this.fs = value == null ? "" : value.toString();
1645 	}
1646 
1647 	/**
1648 	 * Sets IGNORECASE, precomputing its truth value so regexp operations can
1649 	 * test a boolean instead of coercing the raw value on every match.
1650 	 *
1651 	 * @param value new IGNORECASE value
1652 	 */
1653 	public void setIGNORECASE(Object value) {
1654 		this.ignorecase = value == null ? Long.valueOf(0L) : value;
1655 		// gawk: IGNORECASE is active when its value is "nonzero or non-null",
1656 		// i.e. regular AWK truthiness (strnum-aware), not numeric coercion
1657 		this.ignoreCase = toBoolean(this.ignorecase);
1658 	}
1659 
1660 	/**
1661 	 * Get IGNORECASE from the VariableManager.
1662 	 *
1663 	 * @return IGNORECASE value
1664 	 */
1665 	public Object getIGNORECASEVar() {
1666 		return ignorecase;
1667 	}
1668 
1669 	/**
1670 	 * Returns whether IGNORECASE is currently nonzero, making regexp
1671 	 * operations case-insensitive. The truth value is precomputed when
1672 	 * IGNORECASE is assigned.
1673 	 *
1674 	 * @return {@code true} when IGNORECASE is nonzero
1675 	 */
1676 	public boolean isIgnoreCase() {
1677 		return ignoreCase;
1678 	}
1679 
1680 	/**
1681 	 * Returns the {@link Pattern} flags implied by the current
1682 	 * {@code IGNORECASE} setting; dynamic regexps should be compiled with
1683 	 * these flags.
1684 	 *
1685 	 * @return {@link Pattern#CASE_INSENSITIVE} when {@code IGNORECASE} is
1686 	 *         truthy, 0 otherwise
1687 	 */
1688 	public int regexpFlags() {
1689 		return ignoreCase ? Pattern.CASE_INSENSITIVE : 0;
1690 	}
1691 
1692 	/**
1693 	 * {@code sub()} functionality: replaces the first match of {@code ere} in
1694 	 * {@code orig} with {@code repl}, honoring {@code IGNORECASE}. The
1695 	 * substituted text is available through {@link #getReplaceResult()}.
1696 	 *
1697 	 * @param orig original text
1698 	 * @param repl AWK replacement text
1699 	 * @param ere regular expression
1700 	 * @return number of replacements performed (0 or 1)
1701 	 */
1702 	public int replaceFirst(String orig, String repl, String ere) {
1703 		return replace(orig, repl, ere, false);
1704 	}
1705 
1706 	/**
1707 	 * {@code gsub()} functionality: replaces every match of {@code ere} in
1708 	 * {@code orig} with {@code repl}, honoring {@code IGNORECASE}. The
1709 	 * substituted text is available through {@link #getReplaceResult()}.
1710 	 *
1711 	 * @param orig original text
1712 	 * @param repl AWK replacement text
1713 	 * @param ere regular expression
1714 	 * @return number of replacements performed
1715 	 */
1716 	public int replaceAll(String orig, String repl, String ere) {
1717 		return replace(orig, repl, ere, true);
1718 	}
1719 
1720 	private int replace(String orig, String repl, String ere, boolean global) {
1721 		replaceResult.setLength(0);
1722 		String preparedReplacement = prepareReplacement(repl, false);
1723 		Matcher matcher = dynamicPattern(ere).matcher(orig);
1724 		int count = 0;
1725 		while (matcher.find()) {
1726 			count++;
1727 			matcher.appendReplacement(replaceResult, preparedReplacement);
1728 			if (!global) {
1729 				break;
1730 			}
1731 		}
1732 		matcher.appendTail(replaceResult);
1733 		return count;
1734 	}
1735 
1736 	/**
1737 	 * Returns the text produced by the last {@link #replaceFirst} or
1738 	 * {@link #replaceAll} call.
1739 	 *
1740 	 * @return substituted text
1741 	 */
1742 	public String getReplaceResult() {
1743 		return replaceResult.toString();
1744 	}
1745 
1746 	/**
1747 	 * Evaluates the AWK match operator ({@code text ~ regexp}), honoring
1748 	 * {@code IGNORECASE} for both precompiled regexp constants and dynamic
1749 	 * expressions.
1750 	 *
1751 	 * @param text text to match
1752 	 * @param regexp precompiled {@link Pattern} or dynamic regexp text
1753 	 * @return {@code true} when the regexp matches anywhere in the text
1754 	 */
1755 	public boolean matches(String text, Object regexp) {
1756 		if (regexp instanceof Pattern) {
1757 			// find(): AWK's ~ matches anywhere, not the entire string
1758 			return caseAwarePattern((Pattern) regexp).matcher(text).find();
1759 		}
1760 		return dynamicPattern(toAwkString(regexp)).matcher(text).find();
1761 	}
1762 
1763 	/**
1764 	 * {@code match()} functionality: locates {@code ere} in {@code s} honoring
1765 	 * {@code IGNORECASE}, updating {@code RSTART} and {@code RLENGTH}.
1766 	 *
1767 	 * @param s text to search
1768 	 * @param ere regular expression
1769 	 * @return the match position ({@code RSTART}), or 0 when there is no match
1770 	 */
1771 	public int matchPosition(String s, String ere) {
1772 		Matcher matcher = dynamicPattern(ere).matcher(s);
1773 		if (matcher.find()) {
1774 			int start = matcher.start() + 1;
1775 			setRSTART(start);
1776 			setRLENGTH(matcher.end() - matcher.start());
1777 			return start;
1778 		}
1779 		setRSTART(0);
1780 		setRLENGTH(-1);
1781 		return 0;
1782 	}
1783 
1784 	/**
1785 	 * Builds the tokenizer splitting {@code input} by the given separator,
1786 	 * following AWK field-splitting rules ({@code " "} splits on whitespace
1787 	 * runs, {@code ""} splits into characters, a single character is literal)
1788 	 * and honoring {@code IGNORECASE} for regexp separators. A precompiled
1789 	 * {@link Pattern} separator (a regexp literal) is used directly.
1790 	 *
1791 	 * @param input text to split
1792 	 * @param separator field separator: precompiled pattern or text
1793 	 * @return tokenizer producing the split parts
1794 	 */
1795 	public Enumeration<Object> splitTokenizer(String input, Object separator) {
1796 		if (separator instanceof Pattern) {
1797 			return new RegexTokenizer(input, caseAwarePattern((Pattern) separator));
1798 		}
1799 		String fsString = toAwkString(separator);
1800 		if (fsString.equals(" ")) {
1801 			return new StringTokenizer(input);
1802 		}
1803 		if (fsString.isEmpty()) {
1804 			return new CharacterTokenizer(input);
1805 		}
1806 		if (fsString.length() == 1) {
1807 			char fsChar = fsString.charAt(0);
1808 			if (ignoreCase && Character.isLetter(fsChar)) {
1809 				// a letter is regex-safe, so case-insensitive splitting can
1810 				// go through the regexp path
1811 				return new RegexTokenizer(input, dynamicPattern(fsString));
1812 			}
1813 			return new SingleCharacterTokenizer(input, fsChar);
1814 		}
1815 		return new RegexTokenizer(input, dynamicPattern(fsString));
1816 	}
1817 
1818 	/**
1819 	 * Converts an AWK replacement text into a Java {@link Matcher} replacement:
1820 	 * {@code &} becomes the whole match, {@code \&} a literal ampersand, and
1821 	 * {@code $} is escaped.
1822 	 *
1823 	 * @param awkRepl AWK replacement text
1824 	 * @param backreferences whether {@code \N} denotes capture group {@code N},
1825 	 *        as in gawk's {@code gensub()}; when {@code false}, {@code \N} stays
1826 	 *        literal as in {@code sub()} and {@code gsub()}
1827 	 * @return the equivalent Java replacement string
1828 	 */
1829 	public static String prepareReplacement(String awkRepl, boolean backreferences) {
1830 		return prepareReplacement(awkRepl, backreferences ? Integer.MAX_VALUE : -1);
1831 	}
1832 
1833 	/**
1834 	 * Converts an AWK replacement text into a Java {@link Matcher} replacement,
1835 	 * resolving gensub-style backreferences against a known number of capture
1836 	 * groups: {@code \N} beyond {@code maxGroup} is replaced by the empty
1837 	 * string, as gawk does, instead of producing a group reference that would
1838 	 * make the matcher throw.
1839 	 *
1840 	 * @param awkRepl AWK replacement text
1841 	 * @param maxGroup highest valid capture group number, or a negative value
1842 	 *        to disable backreferences entirely ({@code sub()}/{@code gsub()}
1843 	 *        semantics)
1844 	 * @return the equivalent Java replacement string
1845 	 */
1846 	public static String prepareReplacement(String awkRepl, int maxGroup) {
1847 		boolean backreferences = maxGroup >= 0;
1848 		if (awkRepl == null) {
1849 			return "";
1850 		}
1851 
1852 		if ((awkRepl.indexOf('\\') == -1) && (awkRepl.indexOf('$') == -1) && (awkRepl.indexOf('&') == -1)) {
1853 			return awkRepl;
1854 		}
1855 
1856 		StringBuilder javaRepl = new StringBuilder();
1857 		for (int i = 0; i < awkRepl.length(); i++) {
1858 			char c = awkRepl.charAt(i);
1859 
1860 			if (c == '\\' && i == awkRepl.length() - 1) {
1861 				// In gensub mode a trailing backslash is a literal backslash;
1862 				// left bare it would make Matcher.appendReplacement throw. The
1863 				// sub()/gsub() mapping keeps its historical bare form.
1864 				javaRepl.append(backreferences ? "\\\\" : "\\");
1865 				continue;
1866 			}
1867 
1868 			if (c == '\\') {
1869 				i++;
1870 				c = awkRepl.charAt(i);
1871 				if (c == '&') {
1872 					javaRepl.append('&');
1873 					continue;
1874 				} else if (c == '\\') {
1875 					javaRepl.append("\\\\");
1876 					continue;
1877 				} else if (backreferences && Character.isDigit(c)) {
1878 					if (c - '0' <= maxGroup) {
1879 						javaRepl.append('$').append(c);
1880 					}
1881 					// references beyond the pattern's groups expand to the
1882 					// empty string, as in gawk
1883 					continue;
1884 				}
1885 
1886 				javaRepl.append('\\');
1887 			}
1888 
1889 			if (c == '$') {
1890 				javaRepl.append("\\$");
1891 			} else if (c == '&') {
1892 				javaRepl.append("$0");
1893 			} else {
1894 				javaRepl.append(c);
1895 			}
1896 		}
1897 
1898 		return javaRepl.toString();
1899 	}
1900 
1901 	/**
1902 	 * Returns the pattern itself, or its case-insensitive twin when
1903 	 * {@code IGNORECASE} is set. Twins are compiled once and cached here:
1904 	 * the JDK's {@link Pattern#compile(String)} performs no caching of its
1905 	 * own (every call reparses the expression), so dropping this cache would
1906 	 * recompile the regexp on every record matched against a regexp constant.
1907 	 *
1908 	 * @param pattern base pattern
1909 	 * @return pattern honoring the current {@code IGNORECASE} setting
1910 	 */
1911 	public Pattern caseAwarePattern(Pattern pattern) {
1912 		if (!ignoreCase || (pattern.flags() & Pattern.CASE_INSENSITIVE) != 0) {
1913 			return pattern;
1914 		}
1915 		if (caseInsensitivePatterns == null) {
1916 			caseInsensitivePatterns = new IdentityHashMap<Pattern, Pattern>();
1917 		}
1918 		return caseInsensitivePatterns
1919 				.computeIfAbsent(
1920 						pattern,
1921 						base -> Pattern.compile(base.pattern(), base.flags() | Pattern.CASE_INSENSITIVE));
1922 	}
1923 
1924 	/**
1925 	 * Compiles a dynamic (string) regexp with the flags implied by the current
1926 	 * {@code IGNORECASE} setting, caching compiled patterns by expression text:
1927 	 * dynamic regexps are typically reused across records (for example a
1928 	 * {@code gsub(dynstr, ...)} loop), and the JDK's
1929 	 * {@link Pattern#compile(String, int)} reparses the expression on every
1930 	 * call. Each {@code IGNORECASE} setting has its own cache; the settings
1931 	 * cannot share one because {@link Pattern#flags()} reflects inline flag
1932 	 * constructs such as {@code (?i)}, so it cannot tell apart a pattern
1933 	 * compiled under the other setting.
1934 	 *
1935 	 * @param ere dynamic regular expression text
1936 	 * @return the compiled pattern honoring the current {@code IGNORECASE}
1937 	 *         setting
1938 	 */
1939 	public Pattern dynamicPattern(String ere) {
1940 		if (dynamicPatterns == null) {
1941 			dynamicPatterns = new HashMap<String, Pattern>();
1942 			dynamicPatternsIgnoreCase = new HashMap<String, Pattern>();
1943 		}
1944 		Map<String, Pattern> cache = ignoreCase ? dynamicPatternsIgnoreCase : dynamicPatterns;
1945 		Pattern pattern = cache.get(ere);
1946 		if (pattern == null) {
1947 			if (cache.size() >= DYNAMIC_PATTERN_CACHE_LIMIT) {
1948 				cache.clear();
1949 			}
1950 			pattern = Pattern.compile(ere, regexpFlags());
1951 			cache.put(ere, pattern);
1952 		}
1953 		return pattern;
1954 	}
1955 
1956 	/**
1957 	 * Get RS from the VariableManager.
1958 	 *
1959 	 * @return RS value
1960 	 */
1961 	public Object getRSVar() {
1962 		return rs;
1963 	}
1964 
1965 	/**
1966 	 * Returns the current RS value as a string.
1967 	 *
1968 	 * @return current record separator
1969 	 */
1970 	public String getRSString() {
1971 		return rs;
1972 	}
1973 
1974 	/**
1975 	 * Set RS via the VariableManager and apply it to the current reader if any.
1976 	 *
1977 	 * @param value new RS value
1978 	 */
1979 	public void setRS(Object value) {
1980 		this.rs = value == null ? "" : value.toString();
1981 		applyRS(this.rs);
1982 	}
1983 
1984 	/**
1985 	 * Get OFS from the VariableManager.
1986 	 *
1987 	 * @return OFS value
1988 	 */
1989 	public Object getOFSVar() {
1990 		return ofs;
1991 	}
1992 
1993 	/**
1994 	 * Returns the current OFS value as a string.
1995 	 *
1996 	 * @return current output field separator
1997 	 */
1998 	public String getOFSString() {
1999 		return ofs;
2000 	}
2001 
2002 	/**
2003 	 * Set OFS via the VariableManager.
2004 	 *
2005 	 * @param value new OFS value
2006 	 */
2007 	public void setOFS(Object value) {
2008 		this.ofs = value == null ? "" : value.toString();
2009 	}
2010 
2011 	/**
2012 	 * Get ORS from the VariableManager.
2013 	 *
2014 	 * @return ORS value
2015 	 */
2016 	public Object getORSVar() {
2017 		return ors;
2018 	}
2019 
2020 	/**
2021 	 * Returns the current ORS value as a string.
2022 	 *
2023 	 * @return current output record separator
2024 	 */
2025 	public String getORSString() {
2026 		return ors;
2027 	}
2028 
2029 	/**
2030 	 * Set ORS via the VariableManager.
2031 	 *
2032 	 * @param value new ORS value
2033 	 */
2034 	public void setORS(Object value) {
2035 		this.ors = value == null ? "" : value.toString();
2036 	}
2037 
2038 	/**
2039 	 * Get RSTART tracked by JRT (1-based).
2040 	 *
2041 	 * @return current RSTART
2042 	 */
2043 	public Integer getRSTART() {
2044 		return Integer.valueOf(rstart);
2045 	}
2046 
2047 	/**
2048 	 * Set RSTART tracked by JRT (1-based) and mirror to VariableManager.
2049 	 *
2050 	 * @param value new RSTART
2051 	 */
2052 	public void setRSTART(Object value) {
2053 		this.rstart = (int) toLong(value);
2054 	}
2055 
2056 	/**
2057 	 * Get RLENGTH tracked by JRT.
2058 	 *
2059 	 * @return current RLENGTH
2060 	 */
2061 	public Integer getRLENGTH() {
2062 		return Integer.valueOf(rlength);
2063 	}
2064 
2065 	/**
2066 	 * Set RLENGTH tracked by JRT and mirror to VariableManager.
2067 	 *
2068 	 * @param value new RLENGTH
2069 	 */
2070 	public void setRLENGTH(Object value) {
2071 		this.rlength = (int) toLong(value);
2072 	}
2073 
2074 	/**
2075 	 * Get FILENAME as tracked by JRT.
2076 	 *
2077 	 * @return current FILENAME (empty string for stdin/pipe)
2078 	 */
2079 	public Object getFILENAME() {
2080 		return filename == null ? "" : filename;
2081 	}
2082 
2083 	/**
2084 	 * Set FILENAME through VariableManager and update JRT mirror.
2085 	 *
2086 	 * @param name file name to set
2087 	 */
2088 	public void setFILENAMEViaJrt(Object name) {
2089 		this.filename = normalizeRecordValue(name);
2090 	}
2091 
2092 	/**
2093 	 * Get ERRNO as tracked by JRT.
2094 	 *
2095 	 * @return current ERRNO (empty string when no input error is pending)
2096 	 */
2097 	public Object getERRNO() {
2098 		return errno == null ? "" : errno;
2099 	}
2100 
2101 	/**
2102 	 * Set ERRNO tracked by JRT.
2103 	 *
2104 	 * @param value new ERRNO value
2105 	 */
2106 	public void setERRNO(Object value) {
2107 		this.errno = normalizeRecordValue(value);
2108 	}
2109 
2110 	/**
2111 	 * Get ARGIND as tracked by JRT.
2112 	 *
2113 	 * @return ARGV index of the current input file (0 before any file is open)
2114 	 */
2115 	public Object getARGIND() {
2116 		return argind == null ? ZERO : argind;
2117 	}
2118 
2119 	/**
2120 	 * Set ARGIND tracked by JRT.
2121 	 *
2122 	 * @param value new ARGIND value
2123 	 */
2124 	public void setARGIND(Object value) {
2125 		this.argind = normalizeRecordValue(value);
2126 	}
2127 
2128 	/**
2129 	 * Get SUBSEP from the VariableManager.
2130 	 *
2131 	 * @return SUBSEP value
2132 	 */
2133 	public Object getSUBSEPVar() {
2134 		return subsep;
2135 	}
2136 
2137 	/**
2138 	 * Returns the current SUBSEP value as a string.
2139 	 *
2140 	 * @return current multidimensional-array subscript separator
2141 	 */
2142 	public String getSUBSEPString() {
2143 		return subsep;
2144 	}
2145 
2146 	/**
2147 	 * Set SUBSEP via the VariableManager.
2148 	 *
2149 	 * @param value new SUBSEP value
2150 	 */
2151 	public void setSUBSEP(Object value) {
2152 		this.subsep = value == null ? "" : value.toString();
2153 	}
2154 
2155 	/**
2156 	 * Get CONVFMT from the VariableManager.
2157 	 *
2158 	 * @return CONVFMT value
2159 	 */
2160 	public Object getCONVFMTVar() {
2161 		return convfmt;
2162 	}
2163 
2164 	/**
2165 	 * Returns the current CONVFMT value as a string.
2166 	 *
2167 	 * @return current numeric conversion format
2168 	 */
2169 	public String getCONVFMTString() {
2170 		return convfmt;
2171 	}
2172 
2173 	/**
2174 	 * Set CONVFMT via the VariableManager.
2175 	 *
2176 	 * @param value new CONVFMT value
2177 	 */
2178 	public void setCONVFMT(Object value) {
2179 		this.convfmt = value == null ? "" : value.toString();
2180 	}
2181 
2182 	/**
2183 	 * Get OFMT from the VariableManager.
2184 	 *
2185 	 * @return OFMT value
2186 	 */
2187 	public String getOFMTString() {
2188 		return ofmt;
2189 	}
2190 
2191 	/**
2192 	 * Set OFMT via the VariableManager.
2193 	 *
2194 	 * @param value new OFMT value
2195 	 */
2196 	public void setOFMT(Object value) {
2197 		this.ofmt = value == null ? "" : value.toString();
2198 	}
2199 
2200 	/**
2201 	 * Get ARGC from the VariableManager.
2202 	 *
2203 	 * @return ARGC value
2204 	 */
2205 	public Object getARGCVar() {
2206 		return vm.getARGC();
2207 	}
2208 
2209 	/**
2210 	 * Set ARGC via the VariableManager.
2211 	 *
2212 	 * @param value new ARGC value
2213 	 */
2214 	public void setARGC(Object value) {
2215 		vm.assignVariable("ARGC", value);
2216 	}
2217 
2218 	/**
2219 	 * <p>
2220 	 * Setter for the field <code>inputLine</code>.
2221 	 * </p>
2222 	 *
2223 	 * @param inputLineParam input value
2224 	 */
2225 	public void setInputLine(Object inputLineParam) {
2226 		Object inputValue = normalizeRecordValue(inputLineParam);
2227 		this.inputLine = inputValue;
2228 		recordState = new RecordState(inputValue, null);
2229 	}
2230 
2231 	/**
2232 	 * Creates an input-derived AWK scalar value.
2233 	 *
2234 	 * @param value input text
2235 	 * @return input-derived scalar value
2236 	 */
2237 	public Object toInputScalar(Object value) {
2238 		if (value instanceof String) {
2239 			return new StrNum((String) value, decimalSeparator);
2240 		}
2241 		if (value instanceof StrNum) {
2242 			return value;
2243 		}
2244 		if (value == null || value instanceof UninitializedObject) {
2245 			return new StrNum("", decimalSeparator);
2246 		}
2247 		return new StrNum(value.toString(), decimalSeparator);
2248 	}
2249 
2250 	private static Object normalizeRecordValue(Object value) {
2251 		if (value == null || value instanceof UninitializedObject) {
2252 			return "";
2253 		}
2254 		return value;
2255 	}
2256 
2257 	/**
2258 	 * Attempt to consume one record from a structured input source and expose it
2259 	 * as the current input record.
2260 	 *
2261 	 * @param source source strategy that provides records and optional
2262 	 *        pre-split fields
2263 	 * @return {@code true} if a record was consumed; {@code false} when the
2264 	 *         source is exhausted
2265 	 * @throws IOException if the source raises an I/O error
2266 	 */
2267 	public boolean consumeInput(final InputSource source) throws IOException {
2268 		Objects.requireNonNull(source, "source");
2269 		activeSource = source;
2270 		if (!source.nextRecord()) {
2271 			return false;
2272 		}
2273 
2274 		bindConsumedRecord(source);
2275 		return true;
2276 	}
2277 
2278 	/**
2279 	 * Attempt to consume one record from the current input file only, without
2280 	 * ever advancing to the next input file. Used by the per-file main input
2281 	 * loop when BEGINFILE/ENDFILE rules or {@code nextfile} are present, so
2282 	 * that the ENDFILE rules can run at each file boundary.
2283 	 * <p>
2284 	 * When the current input file could not be opened (a pending ERRNO set by
2285 	 * {@link #advanceToNextFile(InputSource)} that no {@code nextfile}
2286 	 * consumed), the usual fatal error is raised, mirroring gawk.
2287 	 * </p>
2288 	 *
2289 	 * @param source source strategy that provides records and optional
2290 	 *        pre-split fields
2291 	 * @return {@code true} if a record was consumed; {@code false} at the end
2292 	 *         of the current input file
2293 	 * @throws IOException if the source raises an I/O error
2294 	 */
2295 	public boolean consumeCurrentFileInput(final InputSource source) throws IOException {
2296 		Objects.requireNonNull(source, "source");
2297 		if (!(source instanceof StreamInputSource)) {
2298 			// Custom input sources behave as a single unnamed input file.
2299 			return consumeInput(source);
2300 		}
2301 		StreamInputSource streamSource = (StreamInputSource) source;
2302 		throwIfCurrentFileUnopened(streamSource);
2303 		activeSource = source;
2304 		if (!streamSource.nextRecordInCurrentFile()) {
2305 			return false;
2306 		}
2307 		bindConsumedRecord(source);
2308 		return true;
2309 	}
2310 
2311 	/**
2312 	 * Attempt to consume one record of the current input file only for
2313 	 * {@code getline target}, returning the input value and leaving the
2314 	 * current input record state untouched. Used instead of
2315 	 * {@link #consumeInputToTarget(InputSource)} while the per-file main
2316 	 * input loop is active, so a {@code getline} in an action never crosses a
2317 	 * file boundary behind the BEGINFILE/ENDFILE rules' back.
2318 	 *
2319 	 * @param source source strategy that provides records and optional
2320 	 *        pre-split fields
2321 	 * @return the consumed input value, or {@code null} at the end of the
2322 	 *         current input file
2323 	 * @throws IOException if the source raises an I/O error
2324 	 */
2325 	public Object consumeCurrentFileInputToTarget(final InputSource source) throws IOException {
2326 		Objects.requireNonNull(source, "source");
2327 		if (!(source instanceof StreamInputSource)) {
2328 			// Custom input sources behave as a single unnamed input file.
2329 			return consumeInputToTarget(source);
2330 		}
2331 		StreamInputSource streamSource = (StreamInputSource) source;
2332 		throwIfCurrentFileUnopened(streamSource);
2333 		activeSource = source;
2334 		materializeCurrentRecord();
2335 		if (!streamSource.nextRecordInCurrentFile()) {
2336 			return null;
2337 		}
2338 
2339 		RecordState inputState = new RecordState(source);
2340 		this.nr++;
2341 		if (countsTowardFNR(source)) {
2342 			this.fnr++;
2343 		}
2344 		return new StrNum(inputState.getRecordText(), decimalSeparator);
2345 	}
2346 
2347 	/**
2348 	 * Raises the gawk-compatible fatal error when the current input file
2349 	 * could not be opened and no BEGINFILE rule bypassed it with
2350 	 * {@code nextfile}.
2351 	 *
2352 	 * @param streamSource the main input source to check
2353 	 */
2354 	private void throwIfCurrentFileUnopened(StreamInputSource streamSource) {
2355 		String openError = streamSource.getCurrentFileOpenError();
2356 		if (openError != null) {
2357 			throw new AwkRuntimeException(
2358 					"cannot open file `" + toAwkString(getFILENAME()) + "' for reading: " + openError);
2359 		}
2360 	}
2361 
2362 	/**
2363 	 * Advance the main input to the next input file, applying pending
2364 	 * {@code name=value} command-line assignments along the way. On success,
2365 	 * FILENAME, FNR, ARGIND, and ERRNO are updated and {@code $0} is cleared,
2366 	 * so the BEGINFILE rules observe the new file. A file that cannot be
2367 	 * opened is still reported as available, with ERRNO carrying the error
2368 	 * description (gawk BEGINFILE error handling).
2369 	 *
2370 	 * @param source source strategy that provides records and optional
2371 	 *        pre-split fields
2372 	 * @return {@code true} when a new input file (or the initial stdin
2373 	 *         stream) is available; {@code false} when input is exhausted
2374 	 * @throws IOException if an I/O error occurs while traversing ARGV
2375 	 */
2376 	public boolean advanceToNextFile(final InputSource source) throws IOException {
2377 		Objects.requireNonNull(source, "source");
2378 		if (source instanceof StreamInputSource) {
2379 			return ((StreamInputSource) source).advanceToNextFile();
2380 		}
2381 		// Custom input sources behave as a single unnamed input file.
2382 		if (syntheticFilePresented) {
2383 			return false;
2384 		}
2385 		syntheticFilePresented = true;
2386 		return true;
2387 	}
2388 
2389 	/**
2390 	 * Returns whether the current input file of the given source failed to
2391 	 * open, leaving a pending error that only a {@code nextfile} statement in
2392 	 * a BEGINFILE rule may bypass.
2393 	 *
2394 	 * @param source source strategy that provides records
2395 	 * @return {@code true} when the current input file could not be opened
2396 	 */
2397 	public boolean hasPendingInputFileError(InputSource source) {
2398 		return source instanceof StreamInputSource
2399 				&& ((StreamInputSource) source).getCurrentFileOpenError() != null;
2400 	}
2401 
2402 	/**
2403 	 * Binds the record just consumed from the given source as the current
2404 	 * input record and updates the NR/FNR counters.
2405 	 *
2406 	 * @param source the source a record was just consumed from
2407 	 */
2408 	private void bindConsumedRecord(InputSource source) {
2409 		inputLine = null;
2410 		recordState = new RecordState(source);
2411 
2412 		this.nr++;
2413 		if (countsTowardFNR(source)) {
2414 			this.fnr++;
2415 		}
2416 	}
2417 
2418 	/**
2419 	 * Returns whether consuming a record from the given source advances FNR,
2420 	 * the per-file record counter. All records of the main command-line input
2421 	 * flow count, including standard input (POSIX defines FNR as the record
2422 	 * number in the <em>current</em> input file, which stdin is). For custom
2423 	 * {@link InputSource} implementations, {@link InputSource#isFromFilenameList()}
2424 	 * keeps controlling FNR, as documented.
2425 	 *
2426 	 * @param source the source a record was just consumed from
2427 	 * @return {@code true} when the record advances FNR
2428 	 */
2429 	private static boolean countsTowardFNR(InputSource source) {
2430 		return source instanceof StreamInputSource || source.isFromFilenameList();
2431 	}
2432 
2433 	/**
2434 	 * Attempt to consume one record from a structured input source for
2435 	 * {@code getline target}, returning the input value and leaving the
2436 	 * current input record state untouched.
2437 	 *
2438 	 * @param source source strategy that provides records and optional
2439 	 *        pre-split fields
2440 	 * @return the consumed input value, or {@code null} when the source is
2441 	 *         exhausted
2442 	 * @throws IOException if the source raises an I/O error
2443 	 */
2444 	public Object consumeInputToTarget(final InputSource source) throws IOException {
2445 		Objects.requireNonNull(source, "source");
2446 		activeSource = source;
2447 		materializeCurrentRecord();
2448 		if (!source.nextRecord()) {
2449 			return null;
2450 		}
2451 
2452 		RecordState inputState = new RecordState(source);
2453 		this.nr++;
2454 		if (countsTowardFNR(source)) {
2455 			this.fnr++;
2456 		}
2457 		return new StrNum(inputState.getRecordText(), decimalSeparator);
2458 	}
2459 
2460 	/**
2461 	 * Consume at most one record from a structured source for expression
2462 	 * evaluation.
2463 	 *
2464 	 * @param source source strategy that provides records and optional
2465 	 *        pre-split fields
2466 	 * @return {@code true} if a record was consumed, {@code false} otherwise
2467 	 * @throws IOException if the source raises an I/O error
2468 	 */
2469 	public boolean consumeInputForEval(InputSource source) throws IOException {
2470 		return consumeInput(source);
2471 	}
2472 
2473 	/**
2474 	 * Initialize {@code $0..$NF} from a pre-split field list.
2475 	 *
2476 	 * @param record current {@code $0} text
2477 	 * @param preFields current fields where index {@code 0} is {@code $1}
2478 	 */
2479 	protected void initializeInputFields(String record, List<String> preFields) {
2480 		recordState = new RecordState(toInputScalar(record), preFields);
2481 	}
2482 
2483 	/**
2484 	 * Splits $0 into $1, $2, etc.
2485 	 * Called when an update to $0 has occurred.
2486 	 */
2487 	public void jrtParseFields() {
2488 		RecordState state = ensureRecordStateForTextMutation();
2489 		state.ensureFieldsMaterialized();
2490 	}
2491 
2492 	/**
2493 	 * Reports whether a record is currently loaded, and therefore whether the
2494 	 * input fields hold anything.
2495 	 *
2496 	 * @return true if at least one input field has been initialized.
2497 	 */
2498 	public boolean hasInputFields() {
2499 		return recordState != null;
2500 	}
2501 
2502 	/**
2503 	 * Adjust the current input field list and $0 when NF is updated by the
2504 	 * AWK script. Fields are either truncated or extended with empty values
2505 	 * so that {@code NF} truly reflects the number of fields.
2506 	 *
2507 	 * @param nfObj New value for NF
2508 	 */
2509 	public void jrtSetNF(Object nfObj) {
2510 		int nf = (int) toDouble(nfObj);
2511 		if (nf < 0) {
2512 			nf = 0;
2513 		}
2514 
2515 		RecordState state = ensureRecordStateForFieldMutation();
2516 		int currentNF = state.getNF();
2517 
2518 		if (nf < currentNF) {
2519 			for (int i = currentNF; i > nf; i--) {
2520 				state.removeField(i - 1);
2521 			}
2522 		} else if (nf > currentNF) {
2523 			for (int i = currentNF + 1; i <= nf; i++) {
2524 				state.addField("");
2525 			}
2526 		}
2527 
2528 		state.markRecordTextDirty();
2529 	}
2530 
2531 	/**
2532 	 * Retrieve the contents of a particular input field.
2533 	 *
2534 	 * @param fieldnumObj Object referring to the field number.
2535 	 * @return Contents of the field.
2536 	 */
2537 	public Object jrtGetInputField(Object fieldnumObj) {
2538 		return jrtGetInputField(parseFieldNumber(fieldnumObj));
2539 	}
2540 
2541 	/**
2542 	 * <p>
2543 	 * jrtGetInputField.
2544 	 * </p>
2545 	 *
2546 	 * @param fieldnum a long
2547 	 * @return a {@link java.lang.Object} object
2548 	 */
2549 	public Object jrtGetInputField(long fieldnum) {
2550 		if (fieldnum < 0 || fieldnum > Integer.MAX_VALUE) {
2551 			throw new AwkRuntimeException("Field $(" + Long.valueOf(fieldnum) + ") is incorrect.");
2552 		}
2553 		if (recordState == null) {
2554 			return BLANK;
2555 		}
2556 		return recordState.getField((int) fieldnum);
2557 	}
2558 
2559 	/**
2560 	 * Stores value_obj into an input field.
2561 	 *
2562 	 * @param valueObj The RHS of the assignment.
2563 	 * @param fieldNum field number to update.
2564 	 * @return A string representation of valueObj.
2565 	 */
2566 	public String jrtSetInputField(Object valueObj, long fieldNum) {
2567 		if (fieldNum > Integer.MAX_VALUE) {
2568 			throw new AwkRuntimeException("Field $(" + Long.valueOf(fieldNum) + ") is incorrect.");
2569 		}
2570 		String value = valueObj == null ? "" : valueObj.toString();
2571 		int fieldIndex = (int) fieldNum;
2572 		RecordState state = ensureRecordStateForFieldMutation();
2573 		if (valueObj instanceof UninitializedObject) {
2574 			if (fieldIndex <= state.getNF()) {
2575 				state.setField(fieldIndex - 1, "");
2576 			}
2577 		} else {
2578 			while (state.getNF() < fieldIndex) {
2579 				state.addField(BLANK);
2580 			}
2581 			state.setField(fieldIndex - 1, valueObj);
2582 		}
2583 		state.markRecordTextDirty();
2584 		return value;
2585 	}
2586 
2587 	/**
2588 	 * Rebuilds {@code $0} from the current field values, joining them with
2589 	 * {@code OFS}, and caches the result as the current input line.
2590 	 * <p>
2591 	 * Does nothing when no record is loaded. Provided for subclasses that mutate
2592 	 * the fields directly rather than through
2593 	 * {@link #jrtSetInputField(Object, long)}, so that {@code $0} stays
2594 	 * consistent with them.
2595 	 * </p>
2596 	 */
2597 	protected void rebuildDollarZeroFromFields() {
2598 		if (recordState != null) {
2599 			recordState.markRecordTextDirty();
2600 			inputLine = recordState.getField(0);
2601 		}
2602 	}
2603 
2604 	private void materializeCurrentRecord() {
2605 		if (recordState != null) {
2606 			recordState.materialize();
2607 		}
2608 	}
2609 
2610 	private RecordState ensureRecordStateForTextMutation() {
2611 		if (recordState == null) {
2612 			recordState = new RecordState(inputLine, null);
2613 		}
2614 		return recordState;
2615 	}
2616 
2617 	private RecordState ensureRecordStateForFieldMutation() {
2618 		RecordState state = ensureRecordStateForTextMutation();
2619 		state.ensureFieldsMaterialized();
2620 		return state;
2621 	}
2622 
2623 	private List<Object> sanitizeFields(List<String> rawFields) {
2624 		List<Object> copy = new ArrayList<Object>(rawFields.size());
2625 		for (String field : rawFields) {
2626 			String value = field == null ? "" : field;
2627 			copy.add(new StrNum(value, decimalSeparator));
2628 		}
2629 		return copy;
2630 	}
2631 
2632 	private List<Object> splitRecordText(String recordText, String fieldSeparator) {
2633 		List<Object> fields = new ArrayList<Object>();
2634 		if (recordText == null || recordText.isEmpty()) {
2635 			return fields;
2636 		}
2637 
2638 		Enumeration<Object> tokenizer = splitTokenizer(recordText, fieldSeparator);
2639 
2640 		while (tokenizer.hasMoreElements()) {
2641 			fields.add(new StrNum((String) tokenizer.nextElement(), decimalSeparator));
2642 		}
2643 		return fields;
2644 	}
2645 
2646 	private static String joinFieldsWithLiteralSeparator(List<Object> fields, String separator) {
2647 		StringBuilder sb = new StringBuilder();
2648 		for (int i = 0; i < fields.size(); i++) {
2649 			if (i > 0) {
2650 				sb.append(separator);
2651 			}
2652 			Object field = fields.get(i);
2653 			sb.append(field == null ? "" : field.toString());
2654 		}
2655 		return sb.toString();
2656 	}
2657 
2658 	private String rebuildRecordTextFromFields(List<Object> fields) {
2659 		// A field assigned a numeric value retains the number itself;
2660 		// reconstituting $0 converts it with CONVFMT, as POSIX requires and
2661 		// gawk does (a string or input-derived field joins verbatim).
2662 		StringBuilder sb = new StringBuilder();
2663 		for (int i = 0; i < fields.size(); i++) {
2664 			if (i > 0) {
2665 				sb.append(ofs);
2666 			}
2667 			Object field = fields.get(i);
2668 			sb.append(field == null ? "" : toAwkString(field));
2669 		}
2670 		return sb.toString();
2671 	}
2672 
2673 	private final class RecordState {
2674 
2675 		private final String fieldSeparatorAtRead;
2676 		private final InputSource source;
2677 		private String recordText;
2678 		private Object recordScalar;
2679 		private List<Object> fields;
2680 		private boolean recordTextAvailable;
2681 		private boolean fieldsAvailable;
2682 		private boolean recordTextDirty;
2683 		private boolean fieldsDirty;
2684 		private boolean recordTextLoadedFromSource;
2685 		private boolean fieldsLoadedFromSource;
2686 
2687 		private RecordState(InputSource source) {
2688 			this(null, null, source);
2689 		}
2690 
2691 		private RecordState(Object recordValue, List<String> rawFields) {
2692 			this(recordValue, rawFields, null);
2693 		}
2694 
2695 		private RecordState(Object recordValue, List<String> rawFields, InputSource source) {
2696 			this.fieldSeparatorAtRead = fs;
2697 			this.source = source;
2698 			if (recordValue != null) {
2699 				this.recordScalar = normalizeRecordValue(recordValue);
2700 				this.recordText = this.recordScalar.toString();
2701 				this.recordTextAvailable = true;
2702 			} else if (rawFields == null && source == null) {
2703 				this.recordScalar = "";
2704 				this.recordText = "";
2705 				this.recordTextAvailable = true;
2706 			}
2707 			if (rawFields != null) {
2708 				this.fields = sanitizeFields(rawFields);
2709 				this.fieldsAvailable = true;
2710 				this.fieldsDirty = false;
2711 			} else {
2712 				this.fieldsAvailable = false;
2713 				this.fieldsDirty = true;
2714 			}
2715 			this.recordTextDirty = false;
2716 		}
2717 
2718 		private void ensureFieldsMaterialized() {
2719 			if (fieldsAvailable && !fieldsDirty) {
2720 				return;
2721 			}
2722 			if (!recordTextDirty) {
2723 				loadFieldsFromSource();
2724 				if (fieldsAvailable && !fieldsDirty) {
2725 					return;
2726 				}
2727 			}
2728 			fields = splitRecordText(getRecordText(), fieldSeparatorAtRead);
2729 			fieldsAvailable = true;
2730 			fieldsDirty = false;
2731 		}
2732 
2733 		private String getRecordText() {
2734 			if (!recordTextAvailable || recordTextDirty) {
2735 				if (recordTextDirty) {
2736 					recordText = rebuildRecordTextFromFields(fields);
2737 					recordScalar = recordText;
2738 				} else {
2739 					loadRecordTextFromSource();
2740 					if (!recordTextAvailable) {
2741 						loadFieldsFromSource();
2742 						if (!fieldsAvailable) {
2743 							throw new IllegalStateException(
2744 									"InputSource must provide record text, fields, or both after nextRecord()");
2745 						}
2746 						recordText = joinFieldsWithLiteralSeparator(fields, fieldSeparatorAtRead);
2747 						recordScalar = new StrNum(recordText, decimalSeparator);
2748 					}
2749 				}
2750 				recordTextAvailable = true;
2751 				recordTextDirty = false;
2752 			}
2753 			return recordText;
2754 		}
2755 
2756 		private int getNF() {
2757 			ensureFieldsMaterialized();
2758 			return fields.size();
2759 		}
2760 
2761 		private Object getField(int fieldIndex) {
2762 			if (fieldIndex == 0) {
2763 				String value = getRecordText();
2764 				if (recordScalar == null) {
2765 					recordScalar = value;
2766 				}
2767 				return recordScalar;
2768 			}
2769 			ensureFieldsMaterialized();
2770 			int zeroBasedIndex = fieldIndex - 1;
2771 			if (zeroBasedIndex < 0 || zeroBasedIndex >= fields.size()) {
2772 				return BLANK;
2773 			}
2774 			return fields.get(zeroBasedIndex);
2775 		}
2776 
2777 		private void setField(int zeroBasedIndex, Object value) {
2778 			ensureFieldsMaterialized();
2779 			fields.set(zeroBasedIndex, normalizeFieldValue(value));
2780 			markRecordTextDirty();
2781 		}
2782 
2783 		private void addField(Object value) {
2784 			ensureFieldsMaterialized();
2785 			fields.add(normalizeFieldValue(value));
2786 			markRecordTextDirty();
2787 		}
2788 
2789 		private Object normalizeFieldValue(Object value) {
2790 			if (value == null) {
2791 				return "";
2792 			}
2793 			return value;
2794 		}
2795 
2796 		private void removeField(int zeroBasedIndex) {
2797 			ensureFieldsMaterialized();
2798 			fields.remove(zeroBasedIndex);
2799 			markRecordTextDirty();
2800 		}
2801 
2802 		private void markRecordTextDirty() {
2803 			recordTextDirty = true;
2804 			recordTextAvailable = fieldsAvailable;
2805 			recordScalar = null;
2806 		}
2807 
2808 		private void materialize() {
2809 			getRecordText();
2810 			ensureFieldsMaterialized();
2811 		}
2812 
2813 		private void loadRecordTextFromSource() {
2814 			if (source == null || recordTextLoadedFromSource) {
2815 				return;
2816 			}
2817 			recordText = source.getRecordText();
2818 			recordTextAvailable = recordText != null;
2819 			if (recordTextAvailable) {
2820 				recordScalar = new StrNum(recordText, decimalSeparator);
2821 			}
2822 			recordTextLoadedFromSource = true;
2823 		}
2824 
2825 		private void loadFieldsFromSource() {
2826 			if (source == null || fieldsLoadedFromSource) {
2827 				return;
2828 			}
2829 			List<String> rawFields = source.getFields();
2830 			fieldsLoadedFromSource = true;
2831 			if (rawFields != null) {
2832 				fields = sanitizeFields(rawFields);
2833 				fieldsAvailable = true;
2834 				fieldsDirty = false;
2835 			}
2836 		}
2837 	}
2838 
2839 	/**
2840 	 * Reads one record from a file for a redirected {@code getline},
2841 	 * translating the outcome into the AWK-visible return code.
2842 	 *
2843 	 * @param fileNameParam name of the file to read from
2844 	 * @return {@code 1} when a record was read (available through
2845 	 *         {@link #jrtGetInputString()}), {@code 0} at end of input, and
2846 	 *         {@code -1} when the file cannot be opened or read, in which case
2847 	 *         ERRNO carries the gawk-style error description
2848 	 * @throws AwkRuntimeException when the filename is the empty string, the
2849 	 *         fatal error gawk raises for a null-string redirection
2850 	 */
2851 	public Integer jrtConsumeFileInputForGetline(String fileNameParam) {
2852 		if (fileNameParam.isEmpty()) {
2853 			throw new AwkRuntimeException("expression for `<' redirection has null string value");
2854 		}
2855 		try {
2856 			if (jrtConsumeFileInput(fileNameParam)) {
2857 				return ONE;
2858 			}
2859 			jrtInputString = "";
2860 			return ZERO;
2861 		} catch (IOException ioe) {
2862 			jrtInputString = "";
2863 			setERRNO(describeOpenFailure(fileNameParam, ioe));
2864 			return MINUS_ONE;
2865 		}
2866 	}
2867 
2868 	/**
2869 	 * Reads one record from the output of a command for a redirected
2870 	 * {@code getline}, translating the outcome into the AWK-visible return
2871 	 * code.
2872 	 *
2873 	 * @param cmdString the command to execute
2874 	 * @return {@code 1} when a record was read (available through
2875 	 *         {@link #jrtGetInputString()}), {@code 0} at end of input, and
2876 	 *         {@code -1} when the process cannot be spawned, in which case
2877 	 *         ERRNO carries the error description
2878 	 * @throws AwkRuntimeException when the command is the empty string, the
2879 	 *         fatal error gawk raises for a null-string redirection
2880 	 */
2881 	public Integer jrtConsumeCommandInputForGetline(String cmdString) {
2882 		if (cmdString.isEmpty()) {
2883 			throw new AwkRuntimeException("expression for `|' redirection has null string value");
2884 		}
2885 		try {
2886 			if (jrtConsumeCommandInput(cmdString)) {
2887 				return ONE;
2888 			}
2889 			jrtInputString = "";
2890 			return ZERO;
2891 		} catch (IOException ioe) {
2892 			jrtInputString = "";
2893 			setERRNO(describeIoReason(ioe));
2894 			return MINUS_ONE;
2895 		}
2896 	}
2897 
2898 	/**
2899 	 * Describes why a file could not be opened for reading, the way gawk
2900 	 * reports it through ERRNO: the strerror-style reason alone, without the
2901 	 * failing path that Java prefixes to its exception messages.
2902 	 *
2903 	 * @param fileNameParam the filename that failed to open
2904 	 * @param ioe the failure raised by the open or read
2905 	 * @return a gawk-style error description
2906 	 */
2907 	private static String describeOpenFailure(String fileNameParam, IOException ioe) {
2908 		if (!isStandardInputName(fileNameParam) && !isNullDeviceName(fileNameParam)) {
2909 			File file = new File(toPlatformFileName(fileNameParam));
2910 			if (file.isDirectory()) {
2911 				return "Is a directory";
2912 			}
2913 			if (!file.exists()) {
2914 				return "No such file or directory";
2915 			}
2916 		}
2917 		return describeIoReason(ioe);
2918 	}
2919 
2920 	/**
2921 	 * Extracts the reason from an I/O exception message, the way gawk reports
2922 	 * failures through ERRNO. Java prefixes the failing path to the reason,
2923 	 * as in {@code path (reason)}; only the reason is kept.
2924 	 *
2925 	 * @param ioe the failure to describe
2926 	 * @return the extracted reason, or "Permission denied" when the exception
2927 	 *         carries no message
2928 	 */
2929 	static String describeIoReason(IOException ioe) {
2930 		String message = ioe.getMessage();
2931 		if (message == null || message.isEmpty()) {
2932 			return "Permission denied";
2933 		}
2934 		int open = message.lastIndexOf('(');
2935 		if (open >= 0 && message.endsWith(")")) {
2936 			return message.substring(open + 1, message.length() - 1);
2937 		}
2938 		return message;
2939 	}
2940 
2941 	/**
2942 	 * Retrieve the record last consumed by a redirected {@code getline}.
2943 	 *
2944 	 * @return the last record read by
2945 	 *         {@link #jrtConsumeFileInputForGetline(String)} or
2946 	 *         {@link #jrtConsumeCommandInputForGetline(String)}
2947 	 */
2948 	public String jrtGetInputString() {
2949 		return jrtInputString;
2950 	}
2951 
2952 	/**
2953 	 * <p>
2954 	 * Getter for the field <code>outputFiles</code>.
2955 	 * </p>
2956 	 *
2957 	 * @return a {@link java.util.Map} object
2958 	 */
2959 	public Map<String, PrintStream> getOutputFiles() {
2960 		Map<String, PrintStream> outputFiles = new HashMap<String, PrintStream>();
2961 		for (Map.Entry<String, FileOutputState> entry : getIoState().fileOutputs.entrySet()) {
2962 			outputFiles.put(entry.getKey(), entry.getValue().sink.getPrintStream());
2963 		}
2964 		return outputFiles;
2965 	}
2966 
2967 	/**
2968 	 * Returns whether the supplied name is the gawk special filename for the
2969 	 * standard input of the process.
2970 	 *
2971 	 * @param fileNameParam name used in a redirection
2972 	 * @return {@code true} for {@code /dev/stdin} and {@code /dev/fd/0}
2973 	 */
2974 	private static boolean isStandardInputName(String fileNameParam) {
2975 		return DEV_STDIN.equals(fileNameParam) || DEV_FD_0.equals(fileNameParam);
2976 	}
2977 
2978 	/**
2979 	 * Returns whether the supplied name is the gawk special filename for the
2980 	 * standard output of the process.
2981 	 *
2982 	 * @param fileNameParam name used in a redirection
2983 	 * @return {@code true} for {@code /dev/stdout} and {@code /dev/fd/1}
2984 	 */
2985 	private static boolean isStandardOutputName(String fileNameParam) {
2986 		return DEV_STDOUT.equals(fileNameParam) || DEV_FD_1.equals(fileNameParam);
2987 	}
2988 
2989 	/**
2990 	 * Returns whether the supplied name is the gawk special filename for the
2991 	 * standard error of the process.
2992 	 *
2993 	 * @param fileNameParam name used in a redirection
2994 	 * @return {@code true} for {@code /dev/stderr} and {@code /dev/fd/2}
2995 	 */
2996 	private static boolean isStandardErrorName(String fileNameParam) {
2997 		return DEV_STDERR.equals(fileNameParam) || DEV_FD_2.equals(fileNameParam);
2998 	}
2999 
3000 	/**
3001 	 * Returns whether the supplied name designates the null device, which reads
3002 	 * as an empty file and discards everything written to it. Both spellings the
3003 	 * platform answers to are recognized: {@code /dev/null} everywhere, and the
3004 	 * native {@code NUL} on Windows, where the file system opens that name as the
3005 	 * device already.
3006 	 * <p>
3007 	 * This is for callers that inspect a filename before opening it, which must
3008 	 * recognize the name instead of relying on the file system, because Windows
3009 	 * does not report its null device as an existing file. Translating a name for
3010 	 * the platform is a narrower question, answered by
3011 	 * {@link #toPlatformFileName(String)}.
3012 	 * </p>
3013 	 *
3014 	 * @param fileNameParam name used in a redirection, in {@code getline} or in
3015 	 *        the {@code ARGV} file list
3016 	 * @return {@code true} when the name designates the null device
3017 	 */
3018 	static boolean isNullDeviceName(String fileNameParam) {
3019 		return DEV_NULL.equals(fileNameParam)
3020 				|| (IS_WINDOWS && WINDOWS_NULL_DEVICE.equalsIgnoreCase(fileNameParam));
3021 	}
3022 
3023 	/**
3024 	 * Maps a script-supplied filename to the name the platform opens it under.
3025 	 * Only the null device is translated, and only on Windows: portable AWK
3026 	 * scripts discard output by redirecting to {@code /dev/null}, which is a real
3027 	 * device on every Unix system but a plain relative path on Windows, where
3028 	 * leaving it untranslated creates and truncates a {@code dev\null} file, or
3029 	 * fails outright when no {@code dev} directory exists. gawk's Windows port
3030 	 * performs the same translation, and, as in gawk, the native {@code NUL}
3031 	 * needs none: Windows opens that name as the device itself.
3032 	 * <p>
3033 	 * Redirections stay keyed by the name the script used, so {@code close()}
3034 	 * takes the original spelling.
3035 	 * </p>
3036 	 *
3037 	 * @param fileNameParam name used in a redirection, in {@code getline} or in
3038 	 *        the {@code ARGV} file list
3039 	 * @return the name to open, which differs from the supplied one only for
3040 	 *         {@code /dev/null} on Windows
3041 	 */
3042 	static String toPlatformFileName(String fileNameParam) {
3043 		return IS_WINDOWS && DEV_NULL.equals(fileNameParam) ? WINDOWS_NULL_DEVICE : fileNameParam;
3044 	}
3045 
3046 	/**
3047 	 * Returns the sink writing to the standard error of the process, creating it
3048 	 * on first use. Every write is flushed so that the records a script sends to
3049 	 * {@code /dev/stderr} interleave with the diagnostics the runtime itself
3050 	 * writes to the same stream.
3051 	 *
3052 	 * @return the {@code /dev/stderr} sink
3053 	 */
3054 	private AwkSink getStandardErrorSink() {
3055 		if (standardErrorSink == null) {
3056 			standardErrorSink = new FlushingAwkSink(warning, locale);
3057 		}
3058 		return standardErrorSink;
3059 	}
3060 
3061 	/**
3062 	 * Resolves the sink used by file redirection. The gawk special filenames
3063 	 * {@code /dev/stdout} and {@code /dev/stderr} (and their {@code /dev/fd/1}
3064 	 * and {@code /dev/fd/2} spellings) are routed to the streams the process
3065 	 * already holds open instead of being opened, and therefore truncated, as
3066 	 * regular files, and {@code /dev/null} designates the platform's null device
3067 	 * on Windows too.
3068 	 *
3069 	 * @param fileNameParam target file name
3070 	 * @param append whether output should be appended
3071 	 * @return the sink that writes to the requested file
3072 	 */
3073 	protected AwkSink getFileAwkSink(String fileNameParam, boolean append) {
3074 		if (isStandardOutputName(fileNameParam)) {
3075 			return openSpecialOutput(fileNameParam, awkSink);
3076 		}
3077 		if (isStandardErrorName(fileNameParam)) {
3078 			return openSpecialOutput(fileNameParam, getStandardErrorSink());
3079 		}
3080 		return getOrCreateFileOutputState(fileNameParam, append).sink;
3081 	}
3082 
3083 	/**
3084 	 * Records that a redirection is open on a standard output special filename,
3085 	 * so that {@code close()} has something to report and to flush.
3086 	 *
3087 	 * @param fileNameParam the special filename being redirected to
3088 	 * @param sink the sink that receives the redirected output
3089 	 * @return the supplied sink
3090 	 */
3091 	private AwkSink openSpecialOutput(String fileNameParam, AwkSink sink) {
3092 		getIoState().specialOutputs.put(fileNameParam, sink);
3093 		return sink;
3094 	}
3095 
3096 	/**
3097 	 * Resolves the sink used by pipe redirection.
3098 	 *
3099 	 * @param cmd command to execute
3100 	 * @return the sink connected to the process stdin
3101 	 */
3102 	protected AwkSink getPipeAwkSink(String cmd) {
3103 		return getOrCreateProcessOutputState(cmd).sink;
3104 	}
3105 
3106 	/**
3107 	 * Writes a standard AWK {@code print} operation to the default output.
3108 	 *
3109 	 * @param values values to print
3110 	 * @throws IOException if the sink cannot be written to
3111 	 */
3112 	public void printDefault(Object[] values) throws IOException {
3113 		awkSink.print(ofs, ors, ofmt, values);
3114 	}
3115 
3116 	/**
3117 	 * Writes a standard AWK {@code print} operation to a redirected file.
3118 	 *
3119 	 * @param fileNameParam target file name
3120 	 * @param append whether output should be appended
3121 	 * @param values values to print; an empty array prints {@code $0}
3122 	 * @throws IOException if the sink cannot be written to
3123 	 */
3124 	public void printToFile(String fileNameParam, boolean append, Object[] values) throws IOException {
3125 		getFileAwkSink(fileNameParam, append).print(ofs, ors, ofmt, values);
3126 	}
3127 
3128 	/**
3129 	 * Writes a standard AWK {@code print} operation to a redirected process.
3130 	 *
3131 	 * @param cmd command to execute
3132 	 * @param values values to print; an empty array prints {@code $0}
3133 	 * @throws IOException if the sink cannot be written to
3134 	 */
3135 	public void printToProcess(String cmd, Object[] values) throws IOException {
3136 		AwkSink sink = getPipeAwkSink(cmd);
3137 		sink.print(ofs, ors, ofmt, values);
3138 		sink.flush();
3139 	}
3140 
3141 	/**
3142 	 * Writes a formatted AWK output string to the specified sink.
3143 	 *
3144 	 * @param format format string passed to {@code printf}
3145 	 * @param values values supplied after the format string
3146 	 * @throws IOException if the sink cannot be written to
3147 	 */
3148 	public void printfDefault(String format, Object[] values) throws IOException {
3149 		awkSink.printf(ofs, ors, ofmt, convfmt, format, values);
3150 	}
3151 
3152 	/**
3153 	 * Formats a string in the same way as AWK's {@code sprintf()} built-in,
3154 	 * through the default output sink and with the current {@code CONVFMT}
3155 	 * value.
3156 	 *
3157 	 * @param format format string passed to {@code sprintf}
3158 	 * @param values arguments supplied after the format string
3159 	 * @return formatted text
3160 	 */
3161 	public String sprintf(String format, Object... values) {
3162 		return awkSink.sprintf(convfmt, format, values);
3163 	}
3164 
3165 	/**
3166 	 * Writes formatted AWK output to a redirected file.
3167 	 *
3168 	 * @param fileNameParam target file name
3169 	 * @param append whether output should be appended
3170 	 * @param format format string passed to {@code printf}
3171 	 * @param values values supplied after the format string
3172 	 * @throws IOException if the sink cannot be written to
3173 	 */
3174 	public void printfToFile(String fileNameParam, boolean append, String format, Object[] values)
3175 			throws IOException {
3176 		AwkSink sink = getFileAwkSink(fileNameParam, append);
3177 		sink.printf(ofs, ors, ofmt, convfmt, format, values);
3178 	}
3179 
3180 	/**
3181 	 * Writes formatted AWK output to a redirected process.
3182 	 *
3183 	 * @param cmd command to execute
3184 	 * @param format format string passed to {@code printf}
3185 	 * @param values values supplied after the format string
3186 	 * @throws IOException if the sink cannot be written to
3187 	 */
3188 	public void printfToProcess(String cmd, String format, Object[] values) throws IOException {
3189 		AwkSink sink = getPipeAwkSink(cmd);
3190 		sink.printf(ofs, ors, ofmt, convfmt, format, values);
3191 		sink.flush();
3192 	}
3193 
3194 	/**
3195 	 * Retrieve the PrintStream which writes to a particular file,
3196 	 * creating the PrintStream if necessary.
3197 	 *
3198 	 * @param fileNameParam The file which to write the contents of the PrintStream.
3199 	 * @param append true to append to the file, false to overwrite the file.
3200 	 * @return a {@link java.io.PrintStream} object
3201 	 */
3202 	public PrintStream jrtGetPrintStream(String fileNameParam, boolean append) {
3203 		return getFileAwkSink(fileNameParam, append).getPrintStream();
3204 	}
3205 
3206 	/**
3207 	 * Reads one record from a file opened by a redirected {@code getline}.
3208 	 * <p>
3209 	 * The reader is opened on first use and kept until it is explicitly closed
3210 	 * or the VM exits. Unlike the main input loop, this transport leaves the
3211 	 * current record ({@code $0} and its fields), NR, FNR, and FILENAME
3212 	 * untouched: gawk documents {@code getline [var] < file} as setting only
3213 	 * the target of the read. The consumed record is exposed through
3214 	 * {@link #jrtGetInputString()}.
3215 	 * </p>
3216 	 * <p>
3217 	 * The gawk special filename {@code /dev/stdin} (and its {@code /dev/fd/0}
3218 	 * spelling) reads the standard input of the process rather than a file of
3219 	 * that name, and {@code /dev/null} reads the platform's null device, which
3220 	 * reports end of input immediately on Windows too.
3221 	 * </p>
3222 	 *
3223 	 * @param fileNameParam name of the file to read from
3224 	 * @return {@code true} when a record was read; {@code false} at end of
3225 	 *         input
3226 	 * @throws java.io.IOException if the file cannot be opened or read; a
3227 	 *         failed open is not cached, so a later {@code getline} from the
3228 	 *         same name retries it
3229 	 */
3230 	public boolean jrtConsumeFileInput(String fileNameParam) throws IOException {
3231 		Map<String, PartitioningReader> fileReaders = getIoState().fileReaders;
3232 		PartitioningReader pr = fileReaders.get(fileNameParam);
3233 		if (pr == null) {
3234 			InputStream inputStream = isStandardInputName(fileNameParam) ?
3235 					standardInput : new FileInputStream(toPlatformFileName(fileNameParam));
3236 			pr = new PartitioningReader(
3237 					new InputStreamReader(inputStream, StandardCharsets.UTF_8),
3238 					this.rs);
3239 			fileReaders.put(fileNameParam, pr);
3240 		}
3241 
3242 		String recordText = pr.readRecord();
3243 		if (recordText == null) {
3244 			return false;
3245 		}
3246 		jrtInputString = recordText;
3247 		return true;
3248 	}
3249 
3250 	private static Process spawnProcess(String cmd, boolean inheritStandardInput) throws IOException {
3251 		ProcessBuilder pb = IS_WINDOWS
3252 		// spawn the process using the Windows shell
3253 				? new ProcessBuilder("cmd.exe", "/c", cmd)
3254 				// spawn the process using the default POSIX shell
3255 				: new ProcessBuilder("/bin/sh", "-c", cmd);
3256 		if (inheritStandardInput) {
3257 			pb.redirectInput(ProcessBuilder.Redirect.INHERIT);
3258 		}
3259 		return pb.start();
3260 	}
3261 
3262 	/**
3263 	 * Declares whether processes spawned on behalf of the script share the
3264 	 * standard input of this JVM. POSIX gives the children of {@code system()}
3265 	 * and of a command pipe the same standard input as awk itself, which is how
3266 	 * terminal-aware commands like {@code "stty size" | getline} find the
3267 	 * controlling terminal. That is only faithful when Jawk reads the real
3268 	 * standard input of the process, which no capture of {@code System.in} can
3269 	 * establish — an embedder may have replaced the stream with
3270 	 * {@code System.setIn} at any point, including before this class
3271 	 * initializes — so eligibility is asserted explicitly by the one caller
3272 	 * that can vouch for it: the command-line entry point of the process.
3273 	 * Everywhere else the flag stays {@code false} and the child's standard
3274 	 * input is closed, since a Java stream cannot be lent to another OS
3275 	 * process, and exposing the host JVM's real descriptor 0 instead would
3276 	 * leak input the embedder never gave to Jawk.
3277 	 *
3278 	 * @param inherit {@code true} when the standard input this run reads is
3279 	 *        the standard input of the JVM process itself
3280 	 */
3281 	public void setSpawnedProcessesInheritStandardInput(boolean inherit) {
3282 		this.spawnedProcessesInheritStandardInput = inherit;
3283 	}
3284 
3285 	/**
3286 	 * Tells whether processes spawned on behalf of the script share the
3287 	 * standard input of this JVM, as declared through
3288 	 * {@link #setSpawnedProcessesInheritStandardInput(boolean)}.
3289 	 *
3290 	 * @return {@code true} when spawned processes inherit the JVM's standard
3291 	 *         input
3292 	 */
3293 	private boolean spawnedProcessInheritsStandardInput() {
3294 		return spawnedProcessesInheritStandardInput;
3295 	}
3296 
3297 	/**
3298 	 * Reads one record from the output of a command spawned by a redirected
3299 	 * {@code getline}.
3300 	 * <p>
3301 	 * The process is spawned on first use and kept until the pipe is
3302 	 * explicitly closed or the VM exits. As with file redirection, the current
3303 	 * record ({@code $0} and its fields), NR, FNR, and FILENAME are left
3304 	 * untouched: gawk documents {@code cmd | getline [var]} as setting only
3305 	 * the target of the read. The consumed record is exposed through
3306 	 * {@link #jrtGetInputString()}.
3307 	 * </p>
3308 	 *
3309 	 * @param cmd the command to execute
3310 	 * @return {@code true} when a record was read; {@code false} at end of
3311 	 *         input
3312 	 * @throws java.io.IOException if the process cannot be spawned; a failed
3313 	 *         spawn is not cached, so a later {@code getline} from the same
3314 	 *         command retries it
3315 	 */
3316 	public boolean jrtConsumeCommandInput(String cmd) throws IOException {
3317 		CommandInputState commandInput = getOrCreateCommandInputState(cmd);
3318 		String recordText = commandInput.reader.readRecord();
3319 		if (recordText == null) {
3320 			return false;
3321 		}
3322 		jrtInputString = recordText;
3323 		return true;
3324 	}
3325 
3326 	/**
3327 	 * Retrieve the PrintStream which shuttles data to stdin for a process,
3328 	 * executing the process if necessary. Threads are created to shuttle the
3329 	 * data to/from the process.
3330 	 *
3331 	 * @param cmd The command to execute.
3332 	 * @return The PrintStream which to write to provide
3333 	 *         input data to the process.
3334 	 */
3335 	public PrintStream jrtSpawnForOutput(String cmd) {
3336 		return getPipeAwkSink(cmd).getPrintStream();
3337 	}
3338 
3339 	private FileOutputState getOrCreateFileOutputState(String fileNameParam, boolean append) {
3340 		IoState state = getIoState();
3341 		FileOutputState outputState = state.fileOutputs.get(fileNameParam);
3342 		if (outputState == null) {
3343 			outputState = createFileOutputState(fileNameParam, append);
3344 			state.fileOutputs.put(fileNameParam, outputState);
3345 		}
3346 		return outputState;
3347 	}
3348 
3349 	private FileOutputState createFileOutputState(String fileNameParam, boolean append) {
3350 		try {
3351 			PrintStream printStream = new PrintStream(
3352 					new FileOutputStream(toPlatformFileName(fileNameParam), append),
3353 					true,
3354 					StandardCharsets.UTF_8.name());
3355 			return new FileOutputState(new OutputStreamAwkSink(printStream, locale));
3356 		} catch (IOException ioe) {
3357 			throw new AwkRuntimeException("Cannot open " + fileNameParam + " for writing: " + ioe);
3358 		}
3359 	}
3360 
3361 	private CommandInputState getOrCreateCommandInputState(String cmd) throws IOException {
3362 		IoState state = getIoState();
3363 		CommandInputState commandInput = state.commandInputs.get(cmd);
3364 		if (commandInput == null) {
3365 			commandInput = createCommandInputState(cmd);
3366 			state.commandInputs.put(cmd, commandInput);
3367 		}
3368 		return commandInput;
3369 	}
3370 
3371 	private CommandInputState createCommandInputState(String cmd) throws IOException {
3372 		Process process = null;
3373 		Thread errorPump = null;
3374 		try {
3375 			// POSIX: the child shares awk's standard input; when that is not
3376 			// possible (embedded execution on a custom stream) it stays closed
3377 			boolean inheritStandardInput = spawnedProcessInheritsStandardInput();
3378 			process = spawnProcess(cmd, inheritStandardInput);
3379 			if (!inheritStandardInput) {
3380 				process.getOutputStream().close();
3381 			}
3382 			errorPump = DataPump.dumpAndReturnThread(cmd + " stderr", process.getErrorStream(), error);
3383 			PartitioningReader reader = new PartitioningReader(
3384 					new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8),
3385 					this.rs);
3386 			return new CommandInputState(process, reader, errorPump);
3387 		} catch (IOException ioe) {
3388 			if (process != null) {
3389 				process.destroy();
3390 			}
3391 			joinDataPump(errorPump);
3392 			throw ioe;
3393 		}
3394 	}
3395 
3396 	private ProcessOutputState getOrCreateProcessOutputState(String cmd) {
3397 		IoState state = getIoState();
3398 		ProcessOutputState outputState = state.processOutputs.get(cmd);
3399 		if (outputState == null) {
3400 			outputState = createProcessOutputState(cmd);
3401 			state.processOutputs.put(cmd, outputState);
3402 		}
3403 		return outputState;
3404 	}
3405 
3406 	private ProcessOutputState createProcessOutputState(String cmd) {
3407 		Process process = null;
3408 		Thread stderrPump = null;
3409 		Thread stdoutPump = null;
3410 		PrintStream processOutput = null;
3411 		try {
3412 			processOutput = awkSink.getPrintStream();
3413 			// the pipe itself is the child's standard input
3414 			process = spawnProcess(cmd, false);
3415 			stderrPump = DataPump.dumpAndReturnThread(cmd + " stderr", process.getErrorStream(), error);
3416 			stdoutPump = DataPump.dumpAndReturnThread(cmd + " stdout", process.getInputStream(), processOutput);
3417 			PrintStream processInput = new PrintStream(process.getOutputStream(), true, StandardCharsets.UTF_8.name());
3418 			return new ProcessOutputState(
3419 					process,
3420 					new OutputStreamAwkSink(processInput, locale),
3421 					processOutput,
3422 					stdoutPump,
3423 					stderrPump);
3424 		} catch (IOException ioe) {
3425 			if (process != null) {
3426 				process.destroy();
3427 			}
3428 			joinDataPump(stdoutPump);
3429 			joinDataPump(stderrPump);
3430 			throw new AwkRuntimeException("Can't spawn " + cmd + ": " + ioe);
3431 		}
3432 	}
3433 
3434 	/**
3435 	 * Attempt to close an open stream, whether it is
3436 	 * an input file, output file, input process, or output
3437 	 * process.
3438 	 * <p>
3439 	 * The specification did not describe AWK behavior
3440 	 * when attempting to close streams/processes with
3441 	 * the same file/command name. In this case,
3442 	 * <em>all</em> open streams with this name
3443 	 * are closed.
3444 	 *
3445 	 * @param fileNameParam The filename/command process to close.
3446 	 * @return Integer(0) upon a successful close, Integer(-1)
3447 	 *         otherwise.
3448 	 */
3449 	public Integer jrtClose(String fileNameParam) {
3450 		boolean b1 = jrtCloseFileReader(fileNameParam);
3451 		boolean b2 = jrtCloseCommandReader(fileNameParam);
3452 		boolean b3 = jrtCloseOutputFile(fileNameParam);
3453 		boolean b4 = jrtCloseOutputStream(fileNameParam);
3454 		boolean b5 = jrtCloseSpecialOutput(fileNameParam);
3455 		// either close will do
3456 		return (b1 || b2 || b3 || b4 || b5) ? ZERO : MINUS_ONE;
3457 	}
3458 
3459 	/**
3460 	 * <p>
3461 	 * jrtCloseAll.
3462 	 * </p>
3463 	 */
3464 	public void jrtCloseAll() {
3465 		IoState state = ioState;
3466 		if (state == null) {
3467 			return;
3468 		}
3469 		Set<String> set = new HashSet<String>();
3470 		for (String s : state.fileReaders.keySet()) {
3471 			set.add(s);
3472 		}
3473 		for (String s : state.commandInputs.keySet()) {
3474 			set.add(s);
3475 		}
3476 		for (String s : state.fileOutputs.keySet()) {
3477 			set.add(s);
3478 		}
3479 		for (String s : state.processOutputs.keySet()) {
3480 			set.add(s);
3481 		}
3482 		for (String s : state.specialOutputs.keySet()) {
3483 			set.add(s);
3484 		}
3485 		for (String s : set) {
3486 			jrtClose(s);
3487 		}
3488 	}
3489 
3490 	/**
3491 	 * Closes a redirection open on one of the standard output special filenames.
3492 	 * The sink is flushed but the stream it writes to is left open: it belongs to
3493 	 * the process, is shared with the runtime's own diagnostics and with the host
3494 	 * application, and gawk likewise closes only its private duplicate of the
3495 	 * descriptor. Redirecting to the same name again therefore works, exactly as
3496 	 * it does in gawk.
3497 	 *
3498 	 * @param fileNameParam the filename passed to {@code close()}
3499 	 * @return {@code true} when a redirection was open on that name and its sink
3500 	 *         was flushed successfully
3501 	 */
3502 	private boolean jrtCloseSpecialOutput(String fileNameParam) {
3503 		IoState state = ioState;
3504 		if (state == null) {
3505 			return false;
3506 		}
3507 		AwkSink sink = state.specialOutputs.remove(fileNameParam);
3508 		if (sink == null) {
3509 			return false;
3510 		}
3511 		try {
3512 			sink.flush();
3513 			return true;
3514 		} catch (IOException ioe) {
3515 			setERRNO(ioe.toString());
3516 			return false;
3517 		}
3518 	}
3519 
3520 	private boolean jrtCloseOutputFile(String fileNameParam) {
3521 		IoState state = ioState;
3522 		if (state == null) {
3523 			return false;
3524 		}
3525 		FileOutputState outputState = state.fileOutputs.remove(fileNameParam);
3526 		if (outputState != null) {
3527 			outputState.sink.getPrintStream().close();
3528 		}
3529 		return outputState != null;
3530 	}
3531 
3532 	private boolean jrtCloseOutputStream(String cmd) {
3533 		IoState state = ioState;
3534 		if (state == null) {
3535 			return false;
3536 		}
3537 		ProcessOutputState outputState = state.processOutputs.remove(cmd);
3538 		if (outputState == null) {
3539 			return false;
3540 		}
3541 		outputState.sink.getPrintStream().close();
3542 		try {
3543 			// wait for the spawned process to finish to make sure
3544 			// all output has been flushed and captured
3545 			outputState.process.waitFor();
3546 			outputState.process.exitValue();
3547 		} catch (InterruptedException ie) {
3548 			Thread.currentThread().interrupt();
3549 			outputState.process.destroyForcibly();
3550 			throw new AwkRuntimeException(
3551 					"Caught exception while waiting for process exit: " + ie);
3552 		} finally {
3553 			joinDataPump(outputState.stdoutPump);
3554 			joinDataPump(outputState.stderrPump);
3555 			outputState.processOutput.flush();
3556 			error.flush();
3557 		}
3558 		return true;
3559 	}
3560 
3561 	private boolean jrtCloseFileReader(String fileNameParam) {
3562 		IoState state = ioState;
3563 		if (state == null) {
3564 			return false;
3565 		}
3566 		PartitioningReader pr = state.fileReaders.get(fileNameParam);
3567 		if (pr == null) {
3568 			return false;
3569 		}
3570 		state.fileReaders.remove(fileNameParam);
3571 		if (isStandardInputName(fileNameParam)) {
3572 			// The standard input of the process is shared with the main input
3573 			// loop and with the host application: drop the record reader but
3574 			// never close the stream behind it. A later getline from the same
3575 			// name reads on from whatever the stream still holds, as it does in
3576 			// gawk, which closes only its private duplicate of the descriptor.
3577 			return true;
3578 		}
3579 		try {
3580 			pr.close();
3581 			return true;
3582 		} catch (IOException ioe) {
3583 			return false;
3584 		}
3585 	}
3586 
3587 	private boolean jrtCloseCommandReader(String cmd) {
3588 		IoState state = ioState;
3589 		if (state == null) {
3590 			return false;
3591 		}
3592 		CommandInputState commandInput = state.commandInputs.remove(cmd);
3593 		if (commandInput == null) {
3594 			return false;
3595 		}
3596 		try {
3597 			commandInput.reader.close();
3598 			try {
3599 				// wait for the process to complete so that all
3600 				// data pumped from the command is captured
3601 				commandInput.process.waitFor();
3602 				commandInput.process.exitValue();
3603 			} catch (InterruptedException ie) {
3604 				Thread.currentThread().interrupt();
3605 				commandInput.process.destroyForcibly();
3606 				throw new AwkRuntimeException(
3607 						"Caught exception while waiting for process exit: " + ie);
3608 			}
3609 			return true;
3610 		} catch (IOException ioe) {
3611 			return false;
3612 		} finally {
3613 			joinDataPump(commandInput.errorPump);
3614 			error.flush();
3615 		}
3616 	}
3617 
3618 	/**
3619 	 * Executes the command specified by cmd and waits
3620 	 * for termination, returning an Integer object
3621 	 * containing the return code.
3622 	 * The command inherits the standard input of the JVM when Jawk reads the
3623 	 * real standard input (CLI runs), as POSIX requires of {@code system()};
3624 	 * otherwise its standard input is closed. Threads are created to shuttle
3625 	 * stdout and stderr of the command to stdout/stderr of the calling
3626 	 * process.
3627 	 *
3628 	 * @param cmd The command to execute.
3629 	 * @return Integer(return_code) of the created
3630 	 *         process. Integer(-1) is returned on an IO error.
3631 	 */
3632 	public Integer jrtSystem(String cmd) {
3633 		try {
3634 			PrintStream processOutput = awkSink.getPrintStream();
3635 			// POSIX: the child shares awk's standard input; when that is not
3636 			// possible (embedded execution on a custom stream) it stays closed
3637 			boolean inheritStandardInput = spawnedProcessInheritsStandardInput();
3638 			Process p = spawnProcess(cmd, inheritStandardInput);
3639 			if (!inheritStandardInput) {
3640 				p.getOutputStream().close();
3641 			}
3642 			Thread errorPump = DataPump.dumpAndReturnThread(cmd + " stderr", p.getErrorStream(), error);
3643 			Thread outputPump = DataPump.dumpAndReturnThread(cmd + " stdout", p.getInputStream(), processOutput);
3644 			boolean interrupted = false;
3645 			int retcode;
3646 			while (true) {
3647 				try {
3648 					retcode = p.waitFor();
3649 					break;
3650 				} catch (InterruptedException ie) {
3651 					// Preserve interrupt and keep waiting so process pipes can close.
3652 					interrupted = true;
3653 				}
3654 			}
3655 			joinDataPump(outputPump);
3656 			joinDataPump(errorPump);
3657 			processOutput.flush();
3658 			error.flush();
3659 			if (interrupted) {
3660 				Thread.currentThread().interrupt();
3661 			}
3662 			return Integer.valueOf(retcode);
3663 		} catch (IOException ioe) {
3664 			return MINUS_ONE;
3665 		}
3666 	}
3667 
3668 	private static void joinDataPump(Thread pump) {
3669 		if (pump == null) {
3670 			return;
3671 		}
3672 		boolean interrupted = false;
3673 		while (true) {
3674 			try {
3675 				pump.join();
3676 				break;
3677 			} catch (InterruptedException ie) {
3678 				interrupted = true;
3679 			}
3680 		}
3681 		if (interrupted) {
3682 			Thread.currentThread().interrupt();
3683 		}
3684 	}
3685 
3686 	/**
3687 	 * <p>
3688 	 * sprintfFunctionNoCatch.
3689 	 * </p>
3690 	 *
3691 	 * @param locale a {@link java.util.Locale} object
3692 	 * @param fmtArg a {@link java.lang.String} object
3693 	 * @param arr an array of {@link java.lang.Object} objects
3694 	 * @return a {@link java.lang.String} object
3695 	 * @throws java.util.IllegalFormatException if any.
3696 	 */
3697 	public static String sprintfNoCatch(Locale locale, String fmtArg, Object... arr) throws IllegalFormatException {
3698 		return String.format(locale, fmtArg, arr);
3699 	}
3700 
3701 	/**
3702 	 * <p>
3703 	 * printfFunctionNoCatch.
3704 	 * </p>
3705 	 *
3706 	 * @param locale a {@link java.util.Locale} object
3707 	 * @param fmtArg a {@link java.lang.String} object
3708 	 * @param arr an array of {@link java.lang.Object} objects
3709 	 */
3710 	public static void printfNoCatch(Locale locale, String fmtArg, Object... arr) {
3711 		System.out.print(sprintfNoCatch(locale, fmtArg, arr));
3712 	}
3713 
3714 	/**
3715 	 * <p>
3716 	 * printfFunctionNoCatch.
3717 	 * </p>
3718 	 *
3719 	 * @param ps a {@link java.io.PrintStream} object
3720 	 * @param locale a {@link java.util.Locale} object
3721 	 * @param fmtArg a {@link java.lang.String} object
3722 	 * @param arr an array of {@link java.lang.Object} objects
3723 	 */
3724 	public static void printfNoCatch(PrintStream ps, Locale locale, String fmtArg, Object... arr) {
3725 		ps.print(sprintfNoCatch(locale, fmtArg, arr));
3726 	}
3727 
3728 	/**
3729 	 * <p>
3730 	 * substr.
3731 	 * </p>
3732 	 *
3733 	 * @param startposObj a {@link java.lang.Object} object
3734 	 * @param str a {@link java.lang.String} object
3735 	 * @return a {@link java.lang.String} object
3736 	 */
3737 	public static String substr(Object startposObj, String str) {
3738 		int startpos = (int) toDouble(startposObj);
3739 		if (startpos <= 0) {
3740 			throw new AwkRuntimeException("2nd arg to substr must be a positive integer");
3741 		}
3742 		if (startpos > str.length()) {
3743 			return "";
3744 		} else {
3745 			return str.substring(startpos - 1);
3746 		}
3747 	}
3748 
3749 	/**
3750 	 * <p>
3751 	 * substr.
3752 	 * </p>
3753 	 *
3754 	 * @param sizeObj a {@link java.lang.Object} object
3755 	 * @param startposObj a {@link java.lang.Object} object
3756 	 * @param str a {@link java.lang.String} object
3757 	 * @return a {@link java.lang.String} object
3758 	 */
3759 	public static String substr(Object sizeObj, Object startposObj, String str) {
3760 		int startpos = (int) toDouble(startposObj);
3761 		if (startpos <= 0) {
3762 			throw new AwkRuntimeException("2nd arg to substr must be a positive integer");
3763 		}
3764 		if (startpos > str.length()) {
3765 			return "";
3766 		}
3767 		int size = (int) toDouble(sizeObj);
3768 		if (size < 0) {
3769 			throw new AwkRuntimeException("3nd arg to substr must be a non-negative integer");
3770 		}
3771 		if (startpos + size > str.length()) {
3772 			return str.substring(startpos - 1);
3773 		} else {
3774 			return str.substring(startpos - 1, startpos + size - 1);
3775 		}
3776 	}
3777 
3778 	/**
3779 	 * <p>
3780 	 * timeSeed.
3781 	 * </p>
3782 	 *
3783 	 * @return a int
3784 	 */
3785 	public static int timeSeed() {
3786 		long l = new Date().getTime();
3787 		long l2 = l % (1000 * 60 * 60 * 24);
3788 		int seed = (int) l2;
3789 		return seed;
3790 	}
3791 
3792 	/**
3793 	 * <p>
3794 	 * newRandom.
3795 	 * </p>
3796 	 *
3797 	 * @param seed a int
3798 	 * @return a {@link java.util.Random} object
3799 	 */
3800 	public static BSDRandom newRandom(int seed) {
3801 		return new BSDRandom(seed);
3802 	}
3803 
3804 	/**
3805 	 * <p>
3806 	 * applyRS.
3807 	 * </p>
3808 	 *
3809 	 * @param rsObj a {@link java.lang.Object} object
3810 	 */
3811 	public void applyRS(Object rsObj) {
3812 		if (activeSource instanceof StreamInputSource) {
3813 			((StreamInputSource) activeSource).setRecordSeparator(rsObj.toString());
3814 		}
3815 	}
3816 }