1 package io.jawk.util;
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.util.HashMap;
26 import java.util.Locale;
27 import java.util.Map;
28 import java.util.Objects;
29 import java.util.concurrent.atomic.AtomicLong;
30 import io.jawk.Awk;
31
32 /**
33 * Reusable behavioral configuration for the Jawk engine.
34 * <p>
35 * Instances hold settings that control how the AWK interpreter behaves
36 * (field separator, locale, sorted keys, initial variables, and default
37 * record separator) but do <em>not</em> carry per-execution state such as
38 * input sources, filename arguments, or output destinations. This
39 * separation allows a single {@code AwkSettings} object to be shared
40 * across many invocations of {@link io.jawk.Awk#eval},
41 * {@link io.jawk.Awk#script}, or {@link io.jawk.Awk#createAvm()}.
42 * </p>
43 * <p>
44 * Output is configured on the execution builder returned by
45 * {@link io.jawk.Awk#script(String)} or {@link io.jawk.Awk#script(io.jawk.AwkProgram)}
46 * and is therefore always per-execution.
47 * </p>
48 *
49 * @author Danny Daglas
50 */
51 public class AwkSettings {
52
53 /**
54 * Shared immutable settings instance representing the default configuration.
55 */
56 public static final AwkSettings DEFAULT_SETTINGS = new ImmutableAwkSettings();
57
58 /**
59 * Contains variable assignments which are applied prior to
60 * executing the script (-v assignments).
61 * The values may be of type <code>Integer</code>,
62 * <code>Double</code>, <code>String</code>,
63 * {@link io.jawk.jrt.AssocArray} (for array variables),
64 * any {@link java.util.Map} that Jawk exposes directly to the script,
65 * or any {@link java.util.List} that Jawk materializes as an array with
66 * zero-based {@link java.lang.Long} keys.
67 * <p>
68 * When a {@link java.util.Map} is provided, the Jawk runtime may mutate it
69 * during execution. Callers must therefore supply a mutable map
70 * implementation. Numeric indices written by the runtime into such maps and
71 * indices derived from {@link java.util.List} values use
72 * {@link java.lang.Long} keys (for example, <code>0L</code>,
73 * <code>1L</code>, ...).
74 * </p>
75 */
76 private final Map<String, Object> variables = new HashMap<String, Object>();
77
78 /**
79 * Initial Field Separator (FS) value.
80 * <code>null</code> means the default FS value.
81 */
82 private volatile String fieldSeparator = null;
83
84 /**
85 * Whether to maintain array keys in sorted order;
86 * <code>false</code> by default.
87 */
88 private volatile boolean useSortedArrayKeys = false;
89
90 /**
91 * Whether to accept gawk-style arrays of arrays syntax such as {@code a[i][j]}
92 * and subarray operands in array-only positions such as {@code split(..., a[i])}.
93 * <code>true</code> by default.
94 */
95 private volatile boolean posix;
96
97 /**
98 * Locale for the output of numbers
99 * <code>US-English</code> by default.
100 */
101 private volatile Locale locale = Locale.US;
102
103 /**
104 * Default value for RS, when not set specifically by the AWK script.
105 * Defaults to {@link Awk#DEFAULT_RS} per POSIX. Platform-specific
106 * end-of-line handling is the responsibility of the input source.
107 */
108 private volatile String defaultRS = Awk.DEFAULT_RS;
109
110 /**
111 * Monotonically increasing counter incremented whenever the settings change.
112 * It allows callers that cache derived runtime state to detect when a new
113 * snapshot must be built.
114 */
115 private final AtomicLong modificationCount = new AtomicLong();
116
117 /**
118 * Creates a settings instance holding Jawk's default configuration, each
119 * field starting at the default documented on its getter.
120 * <p>
121 * Use the setters to depart from it, or the shared {@link #DEFAULT_SETTINGS}
122 * instance when the defaults are enough.
123 * </p>
124 */
125 public AwkSettings() {
126 // Every field is initialized with its documented default.
127 }
128
129 /**
130 * <p>
131 * toDescriptionString.
132 * </p>
133 *
134 * @return a human readable representation of the parameters values.
135 */
136 public String toDescriptionString() {
137 StringBuilder desc = new StringBuilder();
138
139 final char newLine = '\n';
140
141 desc.append("variables = ").append(getVariables()).append(newLine);
142 desc.append("fieldSeparator = ").append(getFieldSeparator()).append(newLine);
143 desc.append("useSortedArrayKeys = ").append(isUseSortedArrayKeys()).append(newLine);
144 desc.append("posix = ").append(isPosix()).append(newLine);
145 return desc.toString();
146 }
147
148 /**
149 * Provides a description of extensions that are enabled/disabled.
150 * The default compiler implementation uses this method
151 * to describe extensions which are compiled into the script.
152 * The description is then provided to the user within the usage.
153 *
154 * @return A description of the extensions which are enabled/disabled.
155 */
156 public String toExtensionDescription() {
157 StringBuilder extensions = new StringBuilder();
158
159 if (isUseSortedArrayKeys()) {
160 extensions.append(", associative array keys are sorted");
161 }
162 if (!isPosix()) {
163 extensions.append(", arrays of arrays");
164 }
165 if (extensions.length() > 0) {
166 return "{extensions: " + extensions.substring(2) + "}";
167 } else {
168 return "{no compiled extensions utilized}";
169 }
170 }
171
172 @SuppressWarnings("unused")
173 private void addInitialVariable(String keyValue) {
174 int equalsIdx = keyValue.indexOf('=');
175 String name = keyValue.substring(0, equalsIdx);
176 String valueString = keyValue.substring(equalsIdx + 1);
177 // note: can overwrite previously defined variables
178 putVariable(name, valueString);
179 }
180
181 /**
182 * Contains variable assignments which are applied prior to
183 * executing the script (-v assignments).
184 * The values may be of type <code>Integer</code>,
185 * <code>Double</code>, <code>String</code>,
186 * {@link io.jawk.jrt.AssocArray} (for array variables),
187 * any {@link java.util.Map} that Jawk exposes directly to the script,
188 * or any {@link java.util.List} that Jawk materializes as an array with
189 * zero-based {@link java.lang.Long} keys.
190 *
191 * @return the variables
192 */
193 public Map<String, Object> getVariables() {
194 synchronized (variables) {
195 return new HashMap<String, Object>(variables);
196 }
197 }
198
199 /**
200 * Returns the number of explicit mutations applied to this settings
201 * instance.
202 * <p>
203 * The value is intended for cache invalidation only; it has no behavioral
204 * meaning other than changing whenever one of the configuration mutators is
205 * called.
206 * </p>
207 *
208 * @return the current modification counter
209 */
210 public long getModificationCount() {
211 return modificationCount.get();
212 }
213
214 /**
215 * Contains variable assignments which are applied prior to
216 * executing the script (-v assignments).
217 * The values may be of type <code>Integer</code>,
218 * <code>Double</code>, <code>String</code>,
219 * {@link io.jawk.jrt.AssocArray} (for array variables),
220 * any {@link java.util.Map} that Jawk exposes directly to the script,
221 * or any {@link java.util.List} that Jawk materializes as an array with
222 * zero-based {@link java.lang.Long} keys.
223 *
224 * @param variables the variables to set
225 */
226 public void setVariables(Map<String, Object> variables) {
227 synchronized (this.variables) {
228 this.variables.clear();
229 this.variables.putAll(variables);
230 }
231 markModified();
232 }
233
234 /**
235 * Put or replace a variable entry.
236 *
237 * @param name Variable name
238 * @param value Variable value
239 */
240 public void putVariable(String name, Object value) {
241 synchronized (variables) {
242 variables.put(name, value);
243 }
244 markModified();
245 }
246
247 /**
248 * Initial Field Separator (FS) value.
249 * <code>null</code> means the default FS value.
250 *
251 * @return the fieldSeparator
252 */
253 public String getFieldSeparator() {
254 return fieldSeparator;
255 }
256
257 /**
258 * Initial Field Separator (FS) value.
259 * <code>null</code> means the default FS value.
260 *
261 * @param fieldSeparator the fieldSeparator to set
262 */
263 public void setFieldSeparator(String fieldSeparator) {
264 this.fieldSeparator = fieldSeparator;
265 markModified();
266 }
267
268 /**
269 * Whether to maintain array keys in sorted order;
270 * <code>false</code> by default.
271 *
272 * @return the useSortedArrayKeys
273 */
274 public boolean isUseSortedArrayKeys() {
275 return useSortedArrayKeys;
276 }
277
278 /**
279 * Whether to maintain array keys in sorted order;
280 * <code>false</code> by default.
281 *
282 * @param useSortedArrayKeys the useSortedArrayKeys to set
283 */
284 public void setUseSortedArrayKeys(boolean useSortedArrayKeys) {
285 this.useSortedArrayKeys = useSortedArrayKeys;
286 markModified();
287 }
288
289 /**
290 * Whether POSIX compile-time behavior is enforced, rejecting gawk syntax
291 * such as arrays of arrays ({@code a[i][j]}, subarray operands like
292 * {@code split(..., a[i])}) and typed regexp literals ({@code @/re/}),
293 * and treating the gawk-specific {@code BEGINFILE} / {@code ENDFILE}
294 * patterns as ordinary identifiers.
295 *
296 * @return {@code true} when POSIX compile-time behavior is enforced
297 */
298 public boolean isPosix() {
299 return posix;
300 }
301
302 /**
303 * Enables or disables POSIX compile-time behavior. When enabled, gawk
304 * syntax such as arrays of arrays ({@code a[i][j]}, subarray operands like
305 * {@code split(..., a[i])} or {@code for (k in a[i])}) and typed regexp
306 * literals ({@code @/re/}) is rejected, and the gawk-specific
307 * {@code BEGINFILE} / {@code ENDFILE} patterns are not special.
308 *
309 * @param posix {@code true} to enforce POSIX compile-time behavior
310 */
311 public void setPosix(boolean posix) {
312 this.posix = posix;
313 markModified();
314 }
315
316 /**
317 * <p>
318 * Getter for the field <code>locale</code>.
319 * </p>
320 *
321 * @return the Locale that will be used for outputting numbers
322 */
323 public Locale getLocale() {
324 return locale;
325 }
326
327 /**
328 * Sets the Locale for outputting numbers.
329 *
330 * @param pLocale The locale to be used (e.g.: <code>Locale.US</code>)
331 */
332 public void setLocale(Locale pLocale) {
333 locale = pLocale == null ? Locale.US : pLocale;
334 markModified();
335 }
336
337 /**
338 * <p>
339 * Getter for the field <code>defaultRS</code>.
340 * </p>
341 *
342 * @return the default RS, when not set by the AWK script
343 */
344 public String getDefaultRS() {
345 return defaultRS;
346 }
347
348 /**
349 * Sets the default RS, when not set by the AWK script
350 *
351 * @param rs The regular expression that separates records
352 */
353 public void setDefaultRS(String rs) {
354 defaultRS = Objects.requireNonNull(rs, "defaultRS");
355 markModified();
356 }
357
358 /**
359 * Records that this settings instance has been mutated, so that callers
360 * caching derived runtime state can detect it through
361 * {@link #getModificationCount()}.
362 * <p>
363 * Every mutator of this class calls it; subclasses adding their own
364 * configuration must do the same.
365 * </p>
366 */
367 protected final void markModified() {
368 modificationCount.incrementAndGet();
369 }
370
371 private static final class ImmutableAwkSettings extends AwkSettings {
372
373 private ImmutableAwkSettings() {
374 super();
375 }
376
377 @Override
378 public void setVariables(Map<String, Object> variables) {
379 throw unsupported();
380 }
381
382 @Override
383 public void putVariable(String name, Object value) {
384 throw unsupported();
385 }
386
387 @Override
388 public void setFieldSeparator(String fieldSeparator) {
389 throw unsupported();
390 }
391
392 @Override
393 public void setUseSortedArrayKeys(boolean useSortedArrayKeys) {
394 throw unsupported();
395 }
396
397 @Override
398 public void setPosix(boolean posix) {
399 throw unsupported();
400 }
401
402 @Override
403 public void setLocale(Locale pLocale) {
404 throw unsupported();
405 }
406
407 @Override
408 public void setDefaultRS(String rs) {
409 throw unsupported();
410 }
411
412 private UnsupportedOperationException unsupported() {
413 return new UnsupportedOperationException("DEFAULT_SETTINGS is immutable");
414 }
415 }
416 }