View Javadoc
1   package org.metricshub.jawk.jrt;
2   
3   /*-
4    * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
5    * Jawk
6    * ჻჻჻჻჻჻
7    * Copyright (C) 2006 - 2025 MetricsHub
8    * ჻჻჻჻჻჻
9    * This program is free software: you can redistribute it and/or modify
10   * it under the terms of the GNU Lesser General Public License as
11   * published by the Free Software Foundation, either version 3 of the
12   * License, or (at your option) any later version.
13   *
14   * This program is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17   * GNU General Lesser Public License for more details.
18   *
19   * You should have received a copy of the GNU General Lesser Public
20   * License along with this program.  If not, see
21   * <http://www.gnu.org/licenses/lgpl-3.0.html>.
22   * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱
23   */
24  
25  // There must be NO imports to org.metricshub.jawk.*,
26  // other than org.metricshub.jawk.jrt which occurs by
27  // default. We wish to house all
28  // required runtime classes in jrt.jar,
29  // not have to refer to jawk.jar!
30  
31  import java.io.FileOutputStream;
32  import java.io.FileInputStream;
33  import java.io.IOException;
34  import java.io.InputStream;
35  import java.io.InputStreamReader;
36  import java.io.PrintStream;
37  import java.nio.charset.StandardCharsets;
38  import java.util.ArrayList;
39  import java.util.Date;
40  import java.util.Enumeration;
41  import java.util.HashMap;
42  import java.util.HashSet;
43  import java.util.IllegalFormatException;
44  import java.util.List;
45  import java.util.Locale;
46  import java.util.Map;
47  import java.util.Set;
48  import java.util.StringTokenizer;
49  import java.util.regex.Matcher;
50  import java.util.regex.Pattern;
51  import java.math.BigDecimal;
52  import org.metricshub.jawk.intermediate.UninitializedObject;
53  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
54  
55  /**
56   * The Jawk runtime coordinator.
57   * The JRT services interpreted and compiled Jawk scripts, mainly
58   * for IO and other non-CPU bound tasks. The goal is to house
59   * service functions into a Java-compiled class rather than
60   * to hand-craft service functions in byte-code, or cut-paste
61   * compiled JVM code into the compiled AWK script. Also,
62   * since these functions are non-CPU bound, the need for
63   * inlining is reduced.
64   * <p>
65   * Variable access is achieved through the VariableManager interface.
66   * The constructor requires a VariableManager instance (which, in
67   * this case, is the compiled Jawk class itself).
68   * <p>
69   * Main services include:
70   * <ul>
71   * <li>File and command output redirection via print(f).
72   * <li>File and command input redirection via getline.
73   * <li>Most built-in AWK functions, such as system(), sprintf(), etc.
74   * <li>Automatic AWK type conversion routines.
75   * <li>IO management for input rule processing.
76   * <li>Random number engine management.
77   * <li>Input field ($0, $1, ...) management.
78   * </ul>
79   * <p>
80   * All static and non-static service methods should be package-private
81   * to the resultant AWK script class rather than public. However,
82   * the resultant script class is not in the <code>org.metricshub.jawk.jrt</code> package
83   * by default, and the user may reassign the resultant script class
84   * to another package. Therefore, all accessed methods are public.
85   *
86   * @see VariableManager
87   * @author Danny Daglas
88   */
89  public class JRT {
90  
91  	private static final boolean IS_WINDOWS = System.getProperty("os.name").indexOf("Windows") >= 0;
92  
93  	private VariableManager vm;
94  
95  	private Map<String, Process> outputProcesses = new HashMap<String, Process>();
96  	private Map<String, PrintStream> outputStreams = new HashMap<String, PrintStream>();
97  	/** PrintStream used for command output */
98  	private PrintStream output = System.out;
99  	/** PrintStream used for command error output */
100 	private PrintStream error = System.err;
101 
102 	// Partitioning reader for stdin.
103 	private PartitioningReader partitioningReader = null;
104 	// Current input line ($0).
105 	private String inputLine = null;
106 	// Current input fields ($0, $1, $2, ...).
107 	private List<String> inputFields = new ArrayList<String>(100);
108 	private AssocArray arglistAa = null;
109 	private int arglistIdx;
110 	private boolean hasFilenames = false;
111 	private static final UninitializedObject BLANK = new UninitializedObject();
112 
113 	private static final Integer ONE = Integer.valueOf(1);
114 	private static final Integer ZERO = Integer.valueOf(0);
115 	private static final Integer MINUS_ONE = Integer.valueOf(-1);
116 	private String jrtInputString;
117 
118 	private Map<String, PartitioningReader> fileReaders = new HashMap<String, PartitioningReader>();
119 	private Map<String, PartitioningReader> commandReaders = new HashMap<String, PartitioningReader>();
120 	private Map<String, Process> commandProcesses = new HashMap<String, Process>();
121 	private Map<String, PrintStream> outputFiles = new HashMap<String, PrintStream>();
122 
123 	/**
124 	 * Create a JRT with a VariableManager
125 	 *
126 	 * @param vm The VariableManager to use with this JRT.
127 	 */
128 	@SuppressFBWarnings(value = "EI_EXPOSE_REP2", justification = "JRT must hold the provided VariableManager for later use")
129 	public JRT(VariableManager vm) {
130 		this.vm = vm;
131 	}
132 
133 	/**
134 	 * Sets the streams for spawned command output and error.
135 	 *
136 	 * @param ps PrintStream to send command output to
137 	 * @param err PrintStream to send command error output to
138 	 */
139 	public void setStreams(PrintStream ps, PrintStream err) {
140 		output = ps == null ? System.out : ps;
141 		error = err == null ? System.err : err;
142 	}
143 
144 	/**
145 	 * Assign all -v variables.
146 	 *
147 	 * @param initialVarMap A map containing all initial variable
148 	 *        names and their values.
149 	 */
150 	public final void assignInitialVariables(Map<String, Object> initialVarMap) {
151 		assert initialVarMap != null;
152 		for (Map.Entry<String, Object> var : initialVarMap.entrySet()) {
153 			vm.assignVariable(var.getKey(), var.getValue());
154 		}
155 	}
156 
157 	/**
158 	 * Called by AVM/compiled modules to assign local
159 	 * environment variables to an associative array
160 	 * (in this case, to ENVIRON).
161 	 *
162 	 * @param aa The associative array to populate with
163 	 *        environment variables. The module asserts that
164 	 *        the associative array is empty prior to population.
165 	 */
166 	public static void assignEnvironmentVariables(AssocArray aa) {
167 		assert aa.keySet().isEmpty();
168 		Map<String, String> env = System.getenv();
169 		for (Map.Entry<String, String> var : env.entrySet()) {
170 			aa.put(var.getKey(), var.getValue());
171 		}
172 	}
173 
174 	/**
175 	 * Convert Strings, Integers, and Doubles to Strings
176 	 * based on the CONVFMT variable contents.
177 	 *
178 	 * @param o Object to convert.
179 	 * @param convfmt The contents of the CONVFMT variable.
180 	 * @return A String representation of o.
181 	 * @param locale a {@link java.util.Locale} object
182 	 */
183 	public static String toAwkString(Object o, String convfmt, Locale locale) {
184 		if (o == null) {
185 			return "";
186 		}
187 		if (o instanceof Number) {
188 			// It is a number, some processing is required here
189 			double d = ((Number) o).doubleValue();
190 			if (isActuallyLong(d)) {
191 				// If an integer, represent it as an integer (no floating point and decimals)
192 				return Long.toString((long) Math.rint(d));
193 			} else {
194 				// It's not a integer, represent it with the specified format
195 				try {
196 					String s = String.format(locale, convfmt, d);
197 					// Surprisingly, while %.6g is the official representation of numbers in AWK
198 					// which should include trailing zeroes, AWK seems to trim them. So, we will
199 					// do the same: trim the trailing zeroes
200 					if ((s.indexOf('.') > -1 || s.indexOf(',') > -1) && (s.indexOf('e') + s.indexOf('E') == -2)) {
201 						while (s.endsWith("0")) {
202 							s = s.substring(0, s.length() - 1);
203 						}
204 						if (s.endsWith(".") || s.endsWith(",")) {
205 							s = s.substring(0, s.length() - 1);
206 						}
207 					}
208 					return s;
209 				} catch (java.util.UnknownFormatConversionException ufce) {
210 					// Impossible case
211 					return "";
212 				}
213 			}
214 		} else {
215 			// It's not a number, easy
216 			return o.toString();
217 		}
218 	}
219 
220 	// not static to use CONVFMT (& possibly OFMT later)
221 	/**
222 	 * Convert a String, Integer, or Double to String
223 	 * based on the OFMT variable contents. Jawk will
224 	 * subsequently use this String for output via print().
225 	 *
226 	 * @param o Object to convert.
227 	 * @param ofmt The contents of the OFMT variable.
228 	 * @return A String representation of o.
229 	 * @param locale a {@link java.util.Locale} object
230 	 */
231 	public static String toAwkStringForOutput(Object o, String ofmt, Locale locale) {
232 		// Even if specified Object o is not officially a number, we try to convert
233 		// it to a Double. Because if it's a literal representation of a number,
234 		// we will need to display it as a number ("12.00" --> 12)
235 		if (!(o instanceof Number)) {
236 			try {
237 				o = new BigDecimal(o.toString()).doubleValue();
238 			} catch (NumberFormatException e) {// NOPMD - EmptyCatchBlock: intentionally ignored
239 			}
240 		}
241 
242 		return toAwkString(o, ofmt, locale);
243 	}
244 
245 	/**
246 	 * Convert a String, Integer, or Double to Double.
247 	 *
248 	 * @param o Object to convert.
249 	 * @return the "double" value of o, or 0 if invalid
250 	 */
251 	public static double toDouble(final Object o) {
252 		if (o == null) {
253 			return 0;
254 		}
255 
256 		if (o instanceof Number) {
257 			return ((Number) o).doubleValue();
258 		}
259 
260 		if (o instanceof Character) {
261 			return (double) ((Character) o).charValue();
262 		}
263 
264 		// Try to convert the string to a number.
265 		String s = o.toString();
266 		int length = s.length();
267 
268 		// Optimization: We don't need to handle strings that are longer than 26 chars
269 		// because a Double cannot be longer than 26 chars when converted to String.
270 		if (length > 26) {
271 			length = 26;
272 		}
273 
274 		// Loop:
275 		// If convervsion fails, try with one character less.
276 		// 25fix will convert to 25 (any numeric prefix will work)
277 		while (length > 0) {
278 			try {
279 				return Double.parseDouble(s.substring(0, length));
280 			} catch (NumberFormatException nfe) {
281 				length--;
282 			}
283 		}
284 
285 		// Failed (not even with one char)
286 		return 0;
287 	}
288 
289 	/**
290 	 * Determines whether a double value actually represents a long integer
291 	 * within the limits of floating point precision.
292 	 *
293 	 * @param d the double value to examine
294 	 * @return {@code true} if {@code d} is effectively an integer
295 	 */
296 	public static boolean isActuallyLong(double d) {
297 		double r = Math.rint(d);
298 		return Math.abs(d - r) < Math.ulp(d);
299 	}
300 
301 	/**
302 	 * Convert a String, Long, or Double to Long.
303 	 *
304 	 * @param o Object to convert.
305 	 * @return the "long" value of o, or 0 if invalid
306 	 */
307 	public static long toLong(final Object o) {
308 		if (o == null) {
309 			return 0;
310 		}
311 
312 		if (o instanceof Number) {
313 			return ((Number) o).longValue();
314 		}
315 
316 		if (o instanceof Character) {
317 			return (long) ((Character) o).charValue();
318 		}
319 
320 		// Try to convert the string to a number.
321 		String s = o.toString();
322 		int length = s.length();
323 
324 		// Optimization: We don't need to handle strings that are longer than 20 chars
325 		// because a Long cannot be longer than 20 chars when converted to String.
326 		if (length > 20) {
327 			length = 20;
328 		}
329 
330 		// Loop:
331 		// If convervsion fails, try with one character less.
332 		// 25fix will convert to 25 (any numeric prefix will work)
333 		while (length > 0) {
334 			try {
335 				return Long.parseLong(s.substring(0, length));
336 			} catch (NumberFormatException nfe) {
337 				length--;
338 			}
339 		}
340 		// Failed (not even with one char)
341 		return 0;
342 	}
343 
344 	/**
345 	 * Compares two objects. Whether to employ less-than, equals, or
346 	 * greater-than checks depends on the mode chosen by the callee.
347 	 * It handles Awk variable rules and type conversion semantics.
348 	 *
349 	 * @param o1 The 1st object.
350 	 * @param o2 the 2nd object.
351 	 * @param mode
352 	 *        <ul>
353 	 *        <li>&lt; 0 - Return true if o1 &lt; o2.
354 	 *        <li>0 - Return true if o1 == o2.
355 	 *        <li>&gt; 0 - Return true if o1 &gt; o2.
356 	 *        </ul>
357 	 * @return a boolean
358 	 */
359 	public static boolean compare2(Object o1, Object o2, int mode) {
360 		// Pre-compute String representations of o1 and o2
361 		String o1String = o1.toString();
362 		String o2String = o2.toString();
363 
364 		// Special case of Uninitialized objects
365 		if (o1 instanceof UninitializedObject) {
366 			if (o2 instanceof UninitializedObject || "".equals(o2String) || "0".equals(o2String)) {
367 				return mode == 0;
368 			} else {
369 				return mode < 0;
370 			}
371 		}
372 		if (o2 instanceof UninitializedObject) {
373 			if ("".equals(o1String) || "0".equals(o1String)) {
374 				return mode == 0;
375 			} else {
376 				return mode > 0;
377 			}
378 		}
379 
380 		if (!(o1 instanceof Number)) {
381 			try {
382 				o1 = new BigDecimal(o1String).doubleValue();
383 			} catch (NumberFormatException nfe) { // NOPMD - ignore invalid number
384 				// ignore invalid number, handled by subsequent logic
385 			}
386 		}
387 		if (!(o2 instanceof Number)) {
388 			try {
389 				o2 = new BigDecimal(o2String).doubleValue();
390 			} catch (NumberFormatException nfe) { // NOPMD - ignore invalid number
391 				// ignore invalid number, handled by subsequent logic
392 			}
393 		}
394 
395 		if ((o1 instanceof Number) && (o2 instanceof Number)) {
396 			if (mode < 0) {
397 				return ((Number) o1).doubleValue() < ((Number) o2).doubleValue();
398 			} else if (mode == 0) {
399 				return ((Number) o1).doubleValue() == ((Number) o2).doubleValue();
400 			} else {
401 				return ((Number) o1).doubleValue() > ((Number) o2).doubleValue();
402 			}
403 		} else {
404 			// string equality usually occurs more often than natural ordering comparison
405 			if (mode == 0) {
406 				return o1String.equals(o2String);
407 			} else if (mode < 0) {
408 				return o1String.compareTo(o2String) < 0;
409 			} else {
410 				return o1String.compareTo(o2String) > 0;
411 			}
412 		}
413 	}
414 
415 	/**
416 	 * Return an object which is numerically equivalent to
417 	 * one plus a given object. For Integers and Doubles,
418 	 * this is similar to o+1. For Strings, attempts are
419 	 * made to convert it to a double first. If the
420 	 * String does not represent a valid Double, 1 is returned.
421 	 *
422 	 * @param o The object to increase.
423 	 * @return o+1 if o is an Integer or Double object, or
424 	 *         if o is a String object and represents a double.
425 	 *         Otherwise, 1 is returned. If the return value
426 	 *         is an integer, an Integer object is returned.
427 	 *         Otherwise, a Double object is returned.
428 	 */
429 	public static Object inc(Object o) {
430 		assert o != null;
431 		double ans;
432 		if (o instanceof Number) {
433 			ans = ((Number) o).doubleValue() + 1;
434 		} else {
435 			try {
436 				ans = Double.parseDouble(o.toString()) + 1;
437 			} catch (NumberFormatException nfe) {
438 				ans = 1;
439 			}
440 		}
441 		if (isActuallyLong(ans)) {
442 			return (long) Math.rint(ans);
443 		} else {
444 			return ans;
445 		}
446 	}
447 
448 	/**
449 	 * Return an object which is numerically equivalent to
450 	 * one minus a given object. For Integers and Doubles,
451 	 * this is similar to o-1. For Strings, attempts are
452 	 * made to convert it to a double first. If the
453 	 * String does not represent a valid Double, -1 is returned.
454 	 *
455 	 * @param o The object to increase.
456 	 * @return o-1 if o is an Integer or Double object, or
457 	 *         if o is a String object and represents a double.
458 	 *         Otherwise, -1 is returned. If the return value
459 	 *         is an integer, an Integer object is returned.
460 	 *         Otherwise, a Double object is returned.
461 	 */
462 	public static Object dec(Object o) {
463 		double ans;
464 		if (o instanceof Number) {
465 			ans = ((Number) o).doubleValue() - 1;
466 		} else {
467 			try {
468 				ans = Double.parseDouble(o.toString()) - 1;
469 			} catch (NumberFormatException nfe) {
470 				ans = 1;
471 			}
472 		}
473 		if (isActuallyLong(ans)) {
474 			return (long) Math.rint(ans);
475 		} else {
476 			return ans;
477 		}
478 	}
479 
480 	// non-static to reference "inputLine"
481 	/**
482 	 * Converts an Integer, Double, String, Pattern,
483 	 * or ConditionPair to a boolean.
484 	 *
485 	 * @param o The object to convert to a boolean.
486 	 * @return For the following class types for o:
487 	 *         <ul>
488 	 *         <li><strong>Integer</strong> - o.intValue() != 0
489 	 *         <li><strong>Long</strong> - o.longValue() != 0
490 	 *         <li><strong>Double</strong> - o.doubleValue() != 0
491 	 *         <li><strong>String</strong> - o.length() &gt; 0
492 	 *         <li><strong>UninitializedObject</strong> - false
493 	 *         <li><strong>Pattern</strong> - $0 ~ o
494 	 *         </ul>
495 	 *         If o is none of these types, an error is thrown.
496 	 */
497 	public final boolean toBoolean(Object o) {
498 		boolean val;
499 		if (o instanceof Integer) {
500 			val = ((Integer) o).intValue() != 0;
501 		} else if (o instanceof Long) {
502 			val = ((Long) o).longValue() != 0;
503 		} else if (o instanceof Double) {
504 			val = ((Double) o).doubleValue() != 0;
505 		} else if (o instanceof String) {
506 			val = (o.toString().length() > 0);
507 		} else if (o instanceof UninitializedObject) {
508 			val = false;
509 		} else if (o instanceof Pattern) {
510 			// match against $0
511 			// ...
512 			Pattern pattern = (Pattern) o;
513 			String s = inputLine == null ? "" : inputLine;
514 			Matcher matcher = pattern.matcher(s);
515 			val = matcher.find();
516 		} else {
517 			throw new Error("Unknown operand_stack type: " + o.getClass() + " for value " + o);
518 		}
519 		return val;
520 	}
521 
522 	/**
523 	 * Splits the string into parts separated by one or more spaces;
524 	 * blank first and last fields are eliminated.
525 	 * This conforms to the 2-argument version of AWK's split function.
526 	 *
527 	 * @param array The array to populate.
528 	 * @param string The string to split.
529 	 * @param convfmt Contents of the CONVFMT variable.
530 	 * @return The number of parts resulting from this split operation.
531 	 * @param locale a {@link java.util.Locale} object
532 	 */
533 	public static int split(Object array, Object string, String convfmt, Locale locale) {
534 		return splitWorker(new StringTokenizer(toAwkString(string, convfmt, locale)), (AssocArray) array);
535 	}
536 
537 	/**
538 	 * Splits the string into parts separated the regular expression fs.
539 	 * This conforms to the 3-argument version of AWK's split function.
540 	 * <p>
541 	 * If fs is blank, it behaves similar to the 2-arg version of
542 	 * AWK's split function.
543 	 *
544 	 * @param fs Field separator regular expression.
545 	 * @param array The array to populate.
546 	 * @param string The string to split.
547 	 * @param convfmt Contents of the CONVFMT variable.
548 	 * @return The number of parts resulting from this split operation.
549 	 * @param locale a {@link java.util.Locale} object
550 	 */
551 	public static int split(Object fs, Object array, Object string, String convfmt, Locale locale) {
552 		String fsString = toAwkString(fs, convfmt, locale);
553 		if (fsString.equals(" ")) {
554 			return splitWorker(new StringTokenizer(toAwkString(string, convfmt, locale)), (AssocArray) array);
555 		} else if (fsString.equals("")) {
556 			return splitWorker(new CharacterTokenizer(toAwkString(string, convfmt, locale)), (AssocArray) array);
557 		} else if (fsString.length() == 1) {
558 			return splitWorker(
559 					new SingleCharacterTokenizer(toAwkString(string, convfmt, locale), fsString.charAt(0)),
560 					(AssocArray) array);
561 		} else {
562 			return splitWorker(new RegexTokenizer(toAwkString(string, convfmt, locale), fsString), (AssocArray) array);
563 		}
564 	}
565 
566 	private static int splitWorker(Enumeration<Object> e, AssocArray aa) {
567 		int cnt = 0;
568 		aa.clear();
569 		while (e.hasMoreElements()) {
570 			aa.put(++cnt, e.nextElement());
571 		}
572 		aa.put(0L, Integer.valueOf(cnt));
573 		return cnt;
574 	}
575 
576 	/**
577 	 * <p>
578 	 * Getter for the field <code>partitioningReader</code>.
579 	 * </p>
580 	 *
581 	 * @return a {@link org.metricshub.jawk.jrt.PartitioningReader} object
582 	 */
583 	@SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "PartitioningReader is shared across callers")
584 	public PartitioningReader getPartitioningReader() {
585 		return partitioningReader;
586 	}
587 
588 	/**
589 	 * <p>
590 	 * Getter for the field <code>inputLine</code>.
591 	 * </p>
592 	 *
593 	 * @return a {@link java.lang.String} object
594 	 */
595 	public String getInputLine() {
596 		return inputLine;
597 	}
598 
599 	/**
600 	 * <p>
601 	 * Setter for the field <code>inputLine</code>.
602 	 * </p>
603 	 *
604 	 * @param inputLine a {@link java.lang.String} object
605 	 */
606 	public void setInputLine(String inputLine) {
607 		this.inputLine = inputLine;
608 	}
609 
610 	/**
611 	 * Attempt to consume one line of input. Input may come from standard input or
612 	 * from files/variable assignments supplied on the command line via
613 	 * {@code ARGV}. Variable assignment arguments are evaluated lazily when
614 	 * encountered.
615 	 *
616 	 * @param input stream used when consuming from standard input
617 	 * @param forGetline {@code true} if the call is for {@code getline}; when
618 	 *        {@code false} the fields of {@code $0} are parsed
619 	 *        automatically
620 	 * @param locale locale used for string conversion
621 	 * @return {@code true} if a line was consumed, {@code false} if no more input
622 	 *         is available
623 	 * @throws IOException upon an IO error
624 	 */
625 	public boolean consumeInput(final InputStream input, boolean forGetline, Locale locale) throws IOException {
626 		initializeArgList(locale);
627 
628 		while (true) {
629 			if ((partitioningReader == null || inputLine == null)
630 					&& !prepareNextReader(input, locale)) {
631 				return false;
632 			}
633 
634 			inputLine = partitioningReader.readRecord();
635 			if (inputLine == null) {
636 				continue;
637 			}
638 
639 			if (!forGetline) {
640 				// For getline the caller will re-acquire $0; otherwise parse fields
641 				jrtParseFields();
642 			}
643 			vm.incNR();
644 			if (partitioningReader.fromFilenameList()) {
645 				vm.incFNR();
646 			}
647 			return true; // NOPMD - loop ends when a line is consumed
648 		}
649 	}
650 
651 	/**
652 	 * Initialize internal state for traversing {@code ARGV}.
653 	 *
654 	 * @param locale locale used for string conversion when inspecting arguments
655 	 */
656 	private void initializeArgList(Locale locale) {
657 		if (arglistAa != null) {
658 			return;
659 		}
660 		arglistAa = (AssocArray) vm.getARGV();
661 		arglistIdx = 1;
662 		hasFilenames = detectFilenames(locale);
663 	}
664 
665 	/**
666 	 * Determine whether {@code ARGV} contains any filename entries (arguments
667 	 * without an equals sign).
668 	 *
669 	 * @param locale locale used for string conversion
670 	 * @return {@code true} if at least one filename was found
671 	 */
672 	private boolean detectFilenames(Locale locale) {
673 		int argc = getArgCount();
674 		for (long i = 1; i < argc; i++) {
675 			if (arglistAa.isIn(i)) {
676 				String arg = toAwkString(arglistAa.get(i), vm.getCONVFMT().toString(), locale);
677 				if (arg.indexOf('=') == -1) {
678 					return true;
679 				}
680 			}
681 		}
682 		return false;
683 	}
684 
685 	/**
686 	 * Retrieve the number of command-line arguments supplied to the script.
687 	 *
688 	 * @return {@code ARGC} converted to an {@code int}
689 	 */
690 	private int getArgCount() {
691 		return Math.toIntExact(toLong(vm.getARGC()));
692 	}
693 
694 	/**
695 	 * Obtain the next valid argument from {@code ARGV}, skipping uninitialized or
696 	 * empty entries.
697 	 *
698 	 * @param locale locale used for string conversion
699 	 * @return the next argument as an AWK string, or {@code null} if none remain
700 	 */
701 	private String nextArgument(Locale locale) {
702 		int argc = getArgCount();
703 		while (arglistIdx <= argc) {
704 			Object o = arglistAa.get(arglistIdx++);
705 			if (!(o instanceof UninitializedObject || o.toString().isEmpty())) {
706 				return toAwkString(o, vm.getCONVFMT().toString(), locale);
707 			}
708 		}
709 		return null;
710 	}
711 
712 	/**
713 	 * Prepare the {@link PartitioningReader} for the next input source. This may
714 	 * be a filename, a variable assignment, or standard input if no filenames
715 	 * remain.
716 	 *
717 	 * @param input default input stream used when reading from standard input
718 	 * @param locale locale used for string conversion
719 	 * @return {@code true} if a reader was prepared, {@code false} if no more
720 	 *         input is available
721 	 * @throws IOException if an I/O error occurs while opening a file
722 	 */
723 	private boolean prepareNextReader(InputStream input, Locale locale) throws IOException {
724 		boolean ready = false;
725 		while (!ready) {
726 			String arg = nextArgument(locale);
727 			if (arg == null) {
728 				if (partitioningReader == null && !hasFilenames) {
729 					partitioningReader = new PartitioningReader(
730 							new InputStreamReader(input, StandardCharsets.UTF_8),
731 							vm.getRS().toString());
732 					vm.setFILENAME("");
733 					return true;
734 				}
735 				return false;
736 			}
737 			if (arg.indexOf('=') != -1) {
738 				setFilelistVariable(arg);
739 				if (partitioningReader == null && !hasFilenames) {
740 					partitioningReader = new PartitioningReader(
741 							new InputStreamReader(input, StandardCharsets.UTF_8),
742 							vm.getRS().toString());
743 					vm.setFILENAME("");
744 					return true;
745 				}
746 				if (partitioningReader != null) {
747 					vm.incNR();
748 				}
749 			} else {
750 				partitioningReader = new PartitioningReader(
751 						new InputStreamReader(new FileInputStream(arg), StandardCharsets.UTF_8),
752 						vm.getRS().toString(),
753 						true);
754 				vm.setFILENAME(arg);
755 				vm.resetFNR();
756 				ready = true;
757 			}
758 		}
759 		return true;
760 	}
761 
762 	/**
763 	 * Read input from stdin, only once, and just for simple AWK expression evaluation
764 	 * <p>
765 	 *
766 	 * @param input Stdin
767 	 * @throws IOException if couldn't read stdin (should never happen, as it's based on a String)
768 	 */
769 	public void setInputLineforEval(InputStream input) throws IOException {
770 		partitioningReader = new PartitioningReader(
771 				new InputStreamReader(input, StandardCharsets.UTF_8),
772 				vm.getRS().toString());
773 		inputLine = partitioningReader.readRecord();
774 		if (inputLine != null) {
775 			jrtParseFields();
776 			vm.incNR();
777 		}
778 	}
779 
780 	/**
781 	 * Parse a {@code name=value} argument from the command line and assign it to
782 	 * the corresponding AWK variable.
783 	 *
784 	 * @param nameValue argument in the form {@code name=value}
785 	 */
786 	private void setFilelistVariable(String nameValue) {
787 		int eqIdx = nameValue.indexOf('=');
788 		// variable name should be non-blank
789 		assert eqIdx >= 0;
790 		if (eqIdx == 0) {
791 			throw new IllegalArgumentException(
792 					"Must have a non-blank variable name in a name=value variable assignment argument.");
793 		}
794 		String name = nameValue.substring(0, eqIdx);
795 		String value = nameValue.substring(eqIdx + 1);
796 		Object obj;
797 		try {
798 			obj = Integer.parseInt(value);
799 		} catch (NumberFormatException nfe) {
800 			try {
801 				obj = Double.parseDouble(value);
802 			} catch (NumberFormatException nfe2) {
803 				obj = value;
804 			}
805 		}
806 		vm.assignVariable(name, obj);
807 	}
808 
809 	/**
810 	 * Splits $0 into $1, $2, etc.
811 	 * Called when an update to $0 has occurred.
812 	 */
813 	public void jrtParseFields() {
814 		String fsString = vm.getFS().toString();
815 		assert inputLine != null;
816 
817 		inputFields.clear();
818 		inputFields.add(inputLine); // $0
819 
820 		if (!inputLine.isEmpty()) {
821 			Enumeration<Object> tokenizer;
822 			if (fsString.equals(" ")) {
823 				tokenizer = new StringTokenizer(inputLine);
824 			} else if (fsString.length() == 1) {
825 				tokenizer = new SingleCharacterTokenizer(inputLine, fsString.charAt(0));
826 			} else if (fsString.equals("")) {
827 				tokenizer = new CharacterTokenizer(inputLine);
828 			} else {
829 				tokenizer = new RegexTokenizer(inputLine, fsString);
830 			}
831 
832 			while (tokenizer.hasMoreElements()) {
833 				inputFields.add((String) tokenizer.nextElement());
834 			}
835 		}
836 
837 		// recalc NF
838 		recalculateNF();
839 	}
840 
841 	private void recalculateNF() {
842 		vm.setNF(Integer.valueOf(inputFields.size() - 1));
843 	}
844 
845 	/**
846 	 * @return true if at least one input field has been initialized.
847 	 */
848 	public boolean hasInputFields() {
849 		return !inputFields.isEmpty();
850 	}
851 
852 	/**
853 	 * Adjust the current input field list and $0 when NF is updated by the
854 	 * AWK script. Fields are either truncated or extended with empty values
855 	 * so that {@code NF} truly reflects the number of fields.
856 	 *
857 	 * @param nfObj New value for NF
858 	 */
859 	public void jrtSetNF(Object nfObj) {
860 		int nf = (int) toDouble(nfObj);
861 		if (nf < 0) {
862 			nf = 0;
863 		}
864 
865 		int currentNF = inputFields.size() - 1;
866 
867 		if (nf < currentNF) {
868 			for (int i = currentNF; i > nf; i--) {
869 				inputFields.remove(i);
870 			}
871 		} else if (nf > currentNF) {
872 			for (int i = currentNF + 1; i <= nf; i++) {
873 				inputFields.add("");
874 			}
875 		}
876 
877 		rebuildDollarZeroFromFields();
878 	}
879 
880 	private static int toFieldNumber(Object o) {
881 		if (o instanceof Number) {
882 			double num = ((Number) o).doubleValue();
883 			if (num < 0) {
884 				throw new RuntimeException("Field $(" + o.toString() + ") is incorrect.");
885 			}
886 			return (int) num;
887 		}
888 
889 		String str = o.toString();
890 		if (str.isEmpty()) {
891 			return 0;
892 		}
893 
894 		try {
895 			double num = new BigDecimal(str).doubleValue();
896 			if (num < 0) {
897 				throw new RuntimeException("Field $(" + o.toString() + ") is incorrect.");
898 			}
899 			return (int) num;
900 		} catch (NumberFormatException nfe) {
901 			return 0;
902 		}
903 	}
904 
905 	/**
906 	 * Retrieve the contents of a particular input field.
907 	 *
908 	 * @param fieldnumObj Object referring to the field number.
909 	 * @return Contents of the field.
910 	 */
911 	public Object jrtGetInputField(Object fieldnumObj) {
912 		return jrtGetInputField(toFieldNumber(fieldnumObj));
913 	}
914 
915 	/**
916 	 * <p>
917 	 * jrtGetInputField.
918 	 * </p>
919 	 *
920 	 * @param fieldnum a int
921 	 * @return a {@link java.lang.Object} object
922 	 */
923 	public Object jrtGetInputField(int fieldnum) {
924 		if (fieldnum < inputFields.size()) {
925 			String retval = inputFields.get(fieldnum);
926 			assert retval != null;
927 			return retval;
928 		} else {
929 			return BLANK;
930 		}
931 	}
932 
933 	/**
934 	 * Stores value_obj into an input field.
935 	 *
936 	 * @param valueObj The RHS of the assignment.
937 	 * @param fieldNum Object referring to the field number.
938 	 * @return A string representation of valueObj.
939 	 */
940 	public String jrtSetInputField(Object valueObj, int fieldNum) {
941 		assert fieldNum >= 1;
942 		assert valueObj != null;
943 		String value = valueObj.toString();
944 		// if the value is BLANK
945 		if (valueObj instanceof UninitializedObject) {
946 			if (fieldNum < inputFields.size()) {
947 				inputFields.set(fieldNum, "");
948 			}
949 		} else {
950 			// append the list to accommodate the new value
951 			for (int i = inputFields.size() - 1; i < fieldNum; i++) {
952 				inputFields.add("");
953 			}
954 			inputFields.set(fieldNum, value);
955 		}
956 		// rebuild $0
957 		rebuildDollarZeroFromFields();
958 		// recalc NF
959 		recalculateNF();
960 		return value;
961 	}
962 
963 	private void rebuildDollarZeroFromFields() {
964 		StringBuilder newDollarZeroSb = new StringBuilder();
965 		String ofs = vm.getOFS().toString();
966 		for (int i = 1; i < inputFields.size(); i++) {
967 			if (i > 1) {
968 				newDollarZeroSb.append(ofs);
969 			}
970 			newDollarZeroSb.append(inputFields.get(i));
971 		}
972 		inputFields.set(0, newDollarZeroSb.toString());
973 	}
974 
975 	/**
976 	 * <p>
977 	 * jrtConsumeFileInputForGetline.
978 	 * </p>
979 	 *
980 	 * @param filename a {@link java.lang.String} object
981 	 * @return a {@link java.lang.Integer} object
982 	 */
983 	public Integer jrtConsumeFileInputForGetline(String filename) {
984 		try {
985 			if (jrtConsumeFileInput(filename)) {
986 				return ONE;
987 			} else {
988 				jrtInputString = "";
989 				return ZERO;
990 			}
991 		} catch (IOException ioe) {
992 			jrtInputString = "";
993 			return MINUS_ONE;
994 		}
995 	}
996 
997 	/**
998 	 * Retrieve the next line of output from a command, executing
999 	 * the command if necessary and store it to $0.
1000 	 *
1001 	 * @param cmdString The command to execute.
1002 	 * @return Integer(1) if successful, Integer(0) if no more
1003 	 *         input is available, Integer(-1) upon an IO error.
1004 	 */
1005 	public Integer jrtConsumeCommandInputForGetline(String cmdString) {
1006 		try {
1007 			if (jrtConsumeCommandInput(cmdString)) {
1008 				return ONE;
1009 			} else {
1010 				jrtInputString = "";
1011 				return ZERO;
1012 			}
1013 		} catch (IOException ioe) {
1014 			jrtInputString = "";
1015 			return MINUS_ONE;
1016 		}
1017 	}
1018 
1019 	/**
1020 	 * Retrieve $0.
1021 	 *
1022 	 * @return The contents of the $0 input field.
1023 	 */
1024 	public String jrtGetInputString() {
1025 		return jrtInputString;
1026 	}
1027 
1028 	/**
1029 	 * <p>
1030 	 * Getter for the field <code>outputFiles</code>.
1031 	 * </p>
1032 	 *
1033 	 * @return a {@link java.util.Map} object
1034 	 */
1035 	@SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "Callers modify the map of output files directly")
1036 	public Map<String, PrintStream> getOutputFiles() {
1037 		return outputFiles;
1038 	}
1039 
1040 	/**
1041 	 * Retrieve the PrintStream which writes to a particular file,
1042 	 * creating the PrintStream if necessary.
1043 	 *
1044 	 * @param filename The file which to write the contents of the PrintStream.
1045 	 * @param append true to append to the file, false to overwrite the file.
1046 	 * @return a {@link java.io.PrintStream} object
1047 	 */
1048 	public PrintStream jrtGetPrintStream(String filename, boolean append) {
1049 		PrintStream ps = outputFiles.get(filename);
1050 		if (ps == null) {
1051 			try {
1052 				ps = new PrintStream(new FileOutputStream(filename, append), true, StandardCharsets.UTF_8.name()); // true
1053 				// =
1054 				// autoflush
1055 				outputFiles.put(filename, ps);
1056 			} catch (IOException ioe) {
1057 				throw new AwkRuntimeException("Cannot open " + filename + " for writing: " + ioe);
1058 			}
1059 		}
1060 		assert ps != null;
1061 		return ps;
1062 	}
1063 
1064 	/**
1065 	 * <p>
1066 	 * jrtConsumeFileInput.
1067 	 * </p>
1068 	 *
1069 	 * @param filename a {@link java.lang.String} object
1070 	 * @return a boolean
1071 	 * @throws java.io.IOException if any.
1072 	 */
1073 	public boolean jrtConsumeFileInput(String filename) throws IOException {
1074 		PartitioningReader pr = fileReaders.get(filename);
1075 		if (pr == null) {
1076 			try {
1077 				pr = new PartitioningReader(
1078 						new InputStreamReader(new FileInputStream(filename), StandardCharsets.UTF_8),
1079 						vm.getRS().toString());
1080 				fileReaders.put(filename, pr);
1081 				vm.setFILENAME(filename);
1082 			} catch (IOException ioe) {
1083 				fileReaders.remove(filename);
1084 				throw ioe;
1085 			}
1086 		}
1087 
1088 		inputLine = pr.readRecord();
1089 		if (inputLine == null) {
1090 			return false;
1091 		} else {
1092 			jrtInputString = inputLine;
1093 			vm.incNR();
1094 			return true;
1095 		}
1096 	}
1097 
1098 	private static Process spawnProcess(String cmd) throws IOException {
1099 		Process p;
1100 
1101 		if (IS_WINDOWS) {
1102 			// spawn the process using the Windows shell
1103 			ProcessBuilder pb = new ProcessBuilder("cmd.exe", "/c", cmd);
1104 			p = pb.start();
1105 		} else {
1106 			// spawn the process using the default POSIX shell
1107 			ProcessBuilder pb = new ProcessBuilder("/bin/sh", "-c", cmd);
1108 			p = pb.start();
1109 		}
1110 
1111 		return p;
1112 	}
1113 
1114 	/**
1115 	 * <p>
1116 	 * jrtConsumeCommandInput.
1117 	 * </p>
1118 	 *
1119 	 * @param cmd a {@link java.lang.String} object
1120 	 * @return a boolean
1121 	 * @throws java.io.IOException if any.
1122 	 */
1123 	public boolean jrtConsumeCommandInput(String cmd) throws IOException {
1124 		PartitioningReader pr = commandReaders.get(cmd);
1125 		if (pr == null) {
1126 			try {
1127 				Process p = spawnProcess(cmd);
1128 				// no input to this process!
1129 				p.getOutputStream().close();
1130 				DataPump.dump(cmd, p.getErrorStream(), System.err);
1131 				commandProcesses.put(cmd, p);
1132 				pr = new PartitioningReader(
1133 						new InputStreamReader(p.getInputStream(), StandardCharsets.UTF_8),
1134 						vm.getRS().toString());
1135 				commandReaders.put(cmd, pr);
1136 				vm.setFILENAME("");
1137 			} catch (IOException ioe) {
1138 				commandReaders.remove(cmd);
1139 				Process p = commandProcesses.get(cmd);
1140 				commandProcesses.remove(cmd);
1141 				if (p != null) {
1142 					p.destroy();
1143 				}
1144 				throw ioe;
1145 			}
1146 		}
1147 
1148 		inputLine = pr.readRecord();
1149 		if (inputLine == null) {
1150 			return false;
1151 		} else {
1152 			jrtInputString = inputLine;
1153 			vm.incNR();
1154 			return true;
1155 		}
1156 	}
1157 
1158 	/**
1159 	 * Retrieve the PrintStream which shuttles data to stdin for a process,
1160 	 * executing the process if necessary. Threads are created to shuttle the
1161 	 * data to/from the process.
1162 	 *
1163 	 * @param cmd The command to execute.
1164 	 * @return The PrintStream which to write to provide
1165 	 *         input data to the process.
1166 	 */
1167 	public PrintStream jrtSpawnForOutput(String cmd) {
1168 		PrintStream ps = outputStreams.get(cmd);
1169 		if (ps == null) {
1170 			Process p;
1171 			try {
1172 				p = spawnProcess(cmd);
1173 				DataPump.dump(cmd, p.getErrorStream(), error);
1174 				DataPump.dump(cmd, p.getInputStream(), output);
1175 			} catch (IOException ioe) {
1176 				throw new AwkRuntimeException("Can't spawn " + cmd + ": " + ioe);
1177 			}
1178 			outputProcesses.put(cmd, p);
1179 			try {
1180 				ps = new PrintStream(p.getOutputStream(), true, StandardCharsets.UTF_8.name()); // true
1181 				// = auto-flush
1182 				outputStreams.put(cmd, ps);
1183 			} catch (java.io.UnsupportedEncodingException e) {
1184 				throw new IllegalStateException(e);
1185 			}
1186 		}
1187 		return ps;
1188 	}
1189 
1190 	/**
1191 	 * Attempt to close an open stream, whether it is
1192 	 * an input file, output file, input process, or output
1193 	 * process.
1194 	 * <p>
1195 	 * The specification did not describe AWK behavior
1196 	 * when attempting to close streams/processes with
1197 	 * the same file/command name. In this case,
1198 	 * <em>all</em> open streams with this name
1199 	 * are closed.
1200 	 *
1201 	 * @param filename The filename/command process to close.
1202 	 * @return Integer(0) upon a successful close, Integer(-1)
1203 	 *         otherwise.
1204 	 */
1205 	public Integer jrtClose(String filename) {
1206 		boolean b1 = jrtCloseFileReader(filename);
1207 		boolean b2 = jrtCloseCommandReader(filename);
1208 		boolean b3 = jrtCloseOutputFile(filename);
1209 		boolean b4 = jrtCloseOutputStream(filename);
1210 		// either close will do
1211 		return (b1 || b2 || b3 || b4) ? ZERO : MINUS_ONE;
1212 	}
1213 
1214 	/**
1215 	 * <p>
1216 	 * jrtCloseAll.
1217 	 * </p>
1218 	 */
1219 	public void jrtCloseAll() {
1220 		Set<String> set = new HashSet<String>();
1221 		for (String s : fileReaders.keySet()) {
1222 			set.add(s);
1223 		}
1224 		for (String s : commandReaders.keySet()) {
1225 			set.add(s);
1226 		}
1227 		for (String s : outputFiles.keySet()) {
1228 			set.add(s);
1229 		}
1230 		for (String s : outputStreams.keySet()) {
1231 			set.add(s);
1232 		}
1233 		for (String s : set) {
1234 			jrtClose(s);
1235 		}
1236 	}
1237 
1238 	private boolean jrtCloseOutputFile(String filename) {
1239 		PrintStream ps = outputFiles.get(filename);
1240 		if (ps != null) {
1241 			ps.close();
1242 			outputFiles.remove(filename);
1243 		}
1244 		return ps != null;
1245 	}
1246 
1247 	private boolean jrtCloseOutputStream(String cmd) {
1248 		Process p = outputProcesses.get(cmd);
1249 		PrintStream ps = outputStreams.get(cmd);
1250 		if (ps == null) {
1251 			return false;
1252 		}
1253 		assert p != null;
1254 		outputProcesses.remove(cmd);
1255 		outputStreams.remove(cmd);
1256 		ps.close();
1257 		try {
1258 			// wait for the spawned process to finish to make sure
1259 			// all output has been flushed and captured
1260 			p.waitFor();
1261 			p.exitValue();
1262 		} catch (InterruptedException ie) {
1263 			throw new AwkRuntimeException(
1264 					"Caught exception while waiting for process exit: " + ie);
1265 		}
1266 		output.flush();
1267 		error.flush();
1268 		return true;
1269 	}
1270 
1271 	private boolean jrtCloseFileReader(String filename) {
1272 		PartitioningReader pr = fileReaders.get(filename);
1273 		if (pr == null) {
1274 			return false;
1275 		}
1276 		fileReaders.remove(filename);
1277 		try {
1278 			pr.close();
1279 			return true;
1280 		} catch (IOException ioe) {
1281 			return false;
1282 		}
1283 	}
1284 
1285 	private boolean jrtCloseCommandReader(String cmd) {
1286 		Process p = commandProcesses.get(cmd);
1287 		PartitioningReader pr = commandReaders.get(cmd);
1288 		if (pr == null) {
1289 			return false;
1290 		}
1291 		assert p != null;
1292 		commandReaders.remove(cmd);
1293 		commandProcesses.remove(cmd);
1294 		try {
1295 			pr.close();
1296 			try {
1297 				// wait for the process to complete so that all
1298 				// data pumped from the command is captured
1299 				p.waitFor();
1300 				p.exitValue();
1301 			} catch (InterruptedException ie) {
1302 				throw new AwkRuntimeException(
1303 						"Caught exception while waiting for process exit: " + ie);
1304 			}
1305 			output.flush();
1306 			error.flush();
1307 			return true;
1308 		} catch (IOException ioe) {
1309 			return false;
1310 		}
1311 	}
1312 
1313 	/**
1314 	 * Executes the command specified by cmd and waits
1315 	 * for termination, returning an Integer object
1316 	 * containing the return code.
1317 	 * stdin to this process is closed while
1318 	 * threads are created to shuttle stdout and
1319 	 * stderr of the command to stdout/stderr
1320 	 * of the calling process.
1321 	 *
1322 	 * @param cmd The command to execute.
1323 	 * @return Integer(return_code) of the created
1324 	 *         process. Integer(-1) is returned on an IO error.
1325 	 */
1326 	public Integer jrtSystem(String cmd) {
1327 		try {
1328 			Process p = spawnProcess(cmd);
1329 			// no input to this process!
1330 			p.getOutputStream().close();
1331 			DataPump.dump(cmd, p.getErrorStream(), error);
1332 			DataPump.dump(cmd, p.getInputStream(), output);
1333 			try {
1334 				int retcode = p.waitFor();
1335 				return Integer.valueOf(retcode);
1336 			} catch (InterruptedException ie) {
1337 				return Integer.valueOf(p.exitValue());
1338 			}
1339 		} catch (IOException ioe) {
1340 			return MINUS_ONE;
1341 		}
1342 	}
1343 
1344 	/**
1345 	 * <p>
1346 	 * sprintfFunctionNoCatch.
1347 	 * </p>
1348 	 *
1349 	 * @param locale a {@link java.util.Locale} object
1350 	 * @param fmtArg a {@link java.lang.String} object
1351 	 * @param arr an array of {@link java.lang.Object} objects
1352 	 * @return a {@link java.lang.String} object
1353 	 * @throws java.util.IllegalFormatException if any.
1354 	 */
1355 	public static String sprintfNoCatch(Locale locale, String fmtArg, Object... arr) throws IllegalFormatException {
1356 		return String.format(locale, fmtArg, arr);
1357 	}
1358 
1359 	/**
1360 	 * <p>
1361 	 * printfFunctionNoCatch.
1362 	 * </p>
1363 	 *
1364 	 * @param locale a {@link java.util.Locale} object
1365 	 * @param fmtArg a {@link java.lang.String} object
1366 	 * @param arr an array of {@link java.lang.Object} objects
1367 	 */
1368 	public static void printfNoCatch(Locale locale, String fmtArg, Object... arr) {
1369 		System.out.print(sprintfNoCatch(locale, fmtArg, arr));
1370 	}
1371 
1372 	/**
1373 	 * <p>
1374 	 * printfFunctionNoCatch.
1375 	 * </p>
1376 	 *
1377 	 * @param ps a {@link java.io.PrintStream} object
1378 	 * @param locale a {@link java.util.Locale} object
1379 	 * @param fmtArg a {@link java.lang.String} object
1380 	 * @param arr an array of {@link java.lang.Object} objects
1381 	 */
1382 	public static void printfNoCatch(PrintStream ps, Locale locale, String fmtArg, Object... arr) {
1383 		ps.print(sprintfNoCatch(locale, fmtArg, arr));
1384 	}
1385 
1386 	/**
1387 	 * Transform the sub/gsub replacement string from Awk syntax
1388 	 * (with '&amp;') to Java (with '$') so it can be used in Matcher.appendReplacement()
1389 	 * <p>
1390 	 * Awk and Java don't use the same syntax for regex replace:
1391 	 * <ul>
1392 	 * <li>Awk uses &amp; to refer to the matched string
1393 	 * <li>Java uses $0, $g, or ${name} to refer to the corresponding match groups
1394 	 * </ul>
1395 	 *
1396 	 * @param awkRepl the replace string passed in sub() and gsub()
1397 	 * @return a string that can be used in Java's Matcher.appendReplacement()
1398 	 */
1399 	public static String prepareReplacement(String awkRepl) {
1400 		// Null
1401 		if (awkRepl == null) {
1402 			return "";
1403 		}
1404 
1405 		// Simple case
1406 		if ((awkRepl.indexOf('\\') == -1) && (awkRepl.indexOf('$') == -1) && (awkRepl.indexOf('&') == -1)) {
1407 			return awkRepl;
1408 		}
1409 
1410 		StringBuilder javaRepl = new StringBuilder();
1411 		for (int i = 0; i < awkRepl.length(); i++) {
1412 			char c = awkRepl.charAt(i);
1413 
1414 			// Backslash
1415 			if (c == '\\' && i < awkRepl.length() - 1) {
1416 				i++;
1417 				c = awkRepl.charAt(i);
1418 				if (c == '&') {
1419 					javaRepl.append('&');
1420 					continue;
1421 				} else if (c == '\\') {
1422 					javaRepl.append("\\\\");
1423 					continue;
1424 				}
1425 
1426 				// For everything else, append the backslash and continue with the logic
1427 				javaRepl.append('\\');
1428 			}
1429 
1430 			if (c == '$') {
1431 				javaRepl.append("\\$");
1432 			} else if (c == '&') {
1433 				javaRepl.append("$0");
1434 			} else {
1435 				javaRepl.append(c);
1436 			}
1437 		}
1438 
1439 		return javaRepl.toString();
1440 	}
1441 
1442 	/**
1443 	 * <p>
1444 	 * replaceFirst.
1445 	 * </p>
1446 	 *
1447 	 * @param origValue a {@link java.lang.String} object
1448 	 * @param repl a {@link java.lang.String} object
1449 	 * @param ere a {@link java.lang.String} object
1450 	 * @param sb a {@link java.lang.StringBuffer} object
1451 	 * @return a {@link java.lang.Integer} object
1452 	 */
1453 	public static Integer replaceFirst(String origValue, String repl, String ere, StringBuffer sb) {
1454 		// remove special meaning for backslash and dollar signs and handle '&'
1455 		repl = prepareReplacement(repl);
1456 
1457 		// Reset provided StringBuffer
1458 		sb.setLength(0);
1459 
1460 		Pattern p = Pattern.compile(ere);
1461 		Matcher m = p.matcher(origValue);
1462 		int cnt = 0;
1463 		if (m.find()) {
1464 			++cnt;
1465 			m.appendReplacement(sb, repl);
1466 		}
1467 		m.appendTail(sb);
1468 		return Integer.valueOf(cnt);
1469 	}
1470 
1471 	/**
1472 	 * Replace all occurrences of the regular expression with specified string
1473 	 *
1474 	 * @param origValue String where replace is done
1475 	 * @param repl Replacement string (with '&amp;' for referring to matching string)
1476 	 * @param ere Regular expression
1477 	 * @param sb StringBuffer we will work on
1478 	 * @return the number of replacements performed
1479 	 */
1480 	public static Integer replaceAll(String origValue, String repl, String ere, StringBuffer sb) {
1481 		// Reset the provided StringBuffer
1482 		sb.setLength(0);
1483 
1484 		// remove special meaning for backslash and dollar signs and handle '&'
1485 		repl = prepareReplacement(repl);
1486 
1487 		Pattern p = Pattern.compile(ere);
1488 		Matcher m = p.matcher(origValue);
1489 		int cnt = 0;
1490 		while (m.find()) {
1491 			++cnt;
1492 			m.appendReplacement(sb, repl);
1493 		}
1494 		m.appendTail(sb);
1495 		return Integer.valueOf(cnt);
1496 	}
1497 
1498 	/**
1499 	 * <p>
1500 	 * substr.
1501 	 * </p>
1502 	 *
1503 	 * @param startposObj a {@link java.lang.Object} object
1504 	 * @param str a {@link java.lang.String} object
1505 	 * @return a {@link java.lang.String} object
1506 	 */
1507 	public static String substr(Object startposObj, String str) {
1508 		int startpos = (int) toDouble(startposObj);
1509 		if (startpos <= 0) {
1510 			throw new AwkRuntimeException("2nd arg to substr must be a positive integer");
1511 		}
1512 		if (startpos > str.length()) {
1513 			return "";
1514 		} else {
1515 			return str.substring(startpos - 1);
1516 		}
1517 	}
1518 
1519 	/**
1520 	 * <p>
1521 	 * substr.
1522 	 * </p>
1523 	 *
1524 	 * @param sizeObj a {@link java.lang.Object} object
1525 	 * @param startposObj a {@link java.lang.Object} object
1526 	 * @param str a {@link java.lang.String} object
1527 	 * @return a {@link java.lang.String} object
1528 	 */
1529 	public static String substr(Object sizeObj, Object startposObj, String str) {
1530 		int startpos = (int) toDouble(startposObj);
1531 		if (startpos <= 0) {
1532 			throw new AwkRuntimeException("2nd arg to substr must be a positive integer");
1533 		}
1534 		if (startpos > str.length()) {
1535 			return "";
1536 		}
1537 		int size = (int) toDouble(sizeObj);
1538 		if (size < 0) {
1539 			throw new AwkRuntimeException("3nd arg to substr must be a non-negative integer");
1540 		}
1541 		if (startpos + size > str.length()) {
1542 			return str.substring(startpos - 1);
1543 		} else {
1544 			return str.substring(startpos - 1, startpos + size - 1);
1545 		}
1546 	}
1547 
1548 	/**
1549 	 * <p>
1550 	 * timeSeed.
1551 	 * </p>
1552 	 *
1553 	 * @return a int
1554 	 */
1555 	public static int timeSeed() {
1556 		long l = new Date().getTime();
1557 		long l2 = l % (1000 * 60 * 60 * 24);
1558 		int seed = (int) l2;
1559 		return seed;
1560 	}
1561 
1562 	/**
1563 	 * <p>
1564 	 * newRandom.
1565 	 * </p>
1566 	 *
1567 	 * @param seed a int
1568 	 * @return a {@link java.util.Random} object
1569 	 */
1570 	public static BSDRandom newRandom(int seed) {
1571 		return new BSDRandom(seed);
1572 	}
1573 
1574 	/**
1575 	 * <p>
1576 	 * applyRS.
1577 	 * </p>
1578 	 *
1579 	 * @param rsObj a {@link java.lang.Object} object
1580 	 */
1581 	public void applyRS(Object rsObj) {
1582 		// if (rsObj.toString().equals(BLANK))
1583 		// rs_obj = DEFAULT_RS_REGEX;
1584 		if (partitioningReader != null) {
1585 			partitioningReader.setRecordSeparator(rsObj.toString());
1586 		}
1587 	}
1588 }