From 8e85bd162de83203d88ed7031ae6981c04e3dede Mon Sep 17 00:00:00 2001 From: Bertrand Martin Date: Tue, 18 Aug 2026 14:58:46 +0200 Subject: [PATCH] Accept POSIX attached option-arguments in the CLI The value-taking short options -f, -v, -F, -L, -K, and -l now accept their value attached to the option letter (-fprog.awk, -vx=1, -F:), as every other AWK implementation does through getopt. The argument is split into the option and its value before dispatch, so option processing boundaries (--, first operand, unknown option after the program text) are unaffected. Fixes #574 Co-Authored-By: Claude Fable 5 --- src/main/java/io/jawk/Cli.java | 41 +++++++++++++++ src/site/markdown/behavior-changes.md | 5 ++ src/site/markdown/cli-reference.md | 1 + src/test/java/io/jawk/CliOptionTest.java | 66 ++++++++++++++++++++++++ 4 files changed, 113 insertions(+) diff --git a/src/main/java/io/jawk/Cli.java b/src/main/java/io/jawk/Cli.java index 7399b92e..0ac2bfb2 100644 --- a/src/main/java/io/jawk/Cli.java +++ b/src/main/java/io/jawk/Cli.java @@ -216,6 +216,13 @@ public void parse(String[] args) { if (arg.length() == 0) { throw new IllegalArgumentException("zero-length argument at position " + (argIdx + 1)); } + if (isAttachedOptionArgument(arg)) { + // POSIX attached option-argument, e.g. -fprog.awk: split it into + // the option and its value so the option branches below see the + // same shape as the separate form -f prog.awk + args = splitAttachedOptionArgument(args, argIdx); + arg = args[argIdx]; + } if (arg.charAt(0) != '-') { // end of options: remaining args are part of the script execution break; @@ -364,6 +371,40 @@ public void parse(String[] args) { } } + /** Single-letter options that take a value and accept it attached. */ + private static final String VALUE_OPTION_LETTERS = "fvFLKl"; + + /** + * Tells whether an argument is a value-taking short option with its value + * attached, as in {@code -fprog.awk} for {@code -f prog.awk}. + * + * @param arg the raw command-line argument + * @return {@code true} when the argument must be split before dispatch + */ + private static boolean isAttachedOptionArgument(String arg) { + return arg.length() > 2 + && arg.charAt(0) == '-' + && VALUE_OPTION_LETTERS.indexOf(arg.charAt(1)) >= 0; + } + + /** + * Splits an attached option-argument in two, so that {@code -fprog.awk} + * becomes {@code -f prog.awk} in the argument array. + * + * @param args full array of arguments + * @param argIdx index of the attached option-argument to split + * @return a copy of the array where the argument at {@code argIdx} is + * replaced by the option and its value as separate elements + */ + private static String[] splitAttachedOptionArgument(String[] args, int argIdx) { + String[] split = new String[args.length + 1]; + System.arraycopy(args, 0, split, 0, argIdx); + split[argIdx] = args[argIdx].substring(0, 2); + split[argIdx + 1] = args[argIdx].substring(2); + System.arraycopy(args, argIdx + 1, split, argIdx + 2, args.length - argIdx - 1); + return split; + } + /** * Ensures that the current command-line option is followed by a value. * diff --git a/src/site/markdown/behavior-changes.md b/src/site/markdown/behavior-changes.md index a9a33687..328e82f0 100644 --- a/src/site/markdown/behavior-changes.md +++ b/src/site/markdown/behavior-changes.md @@ -20,6 +20,11 @@ released version automatically via .github/scripts/stamp-behavior-changes.sh. ## Unreleased +- The CLI now accepts POSIX attached option-arguments for the value-taking short options: + `-fprog.awk`, `-vx=1`, `-F:` (and the Jawk-specific `-L`, `-K`, `-l`) are equivalent to + `-f prog.awk`, `-v x=1`, `-F :`, as in gawk, mawk, BWK awk, and goawk. Previously the glued + form was rejected with `Unknown parameter` + ([#574](https://github.com/jawkio/jawk/issues/574)). - An input-derived value whose text is a number surrounded by blanks — a record like `" 12 "`, a `getline var` result read from padded input, a `split()` piece under a non-default separator — is now recognized as a POSIX numeric string, so `$0 == 12` is true for the record diff --git a/src/site/markdown/cli-reference.md b/src/site/markdown/cli-reference.md index b2bb5931..8a01397b 100644 --- a/src/site/markdown/cli-reference.md +++ b/src/site/markdown/cli-reference.md @@ -44,6 +44,7 @@ java -jar jawk-${project.version}-standalone.jar --list-ext > - An operand containing `=` is treated as an AWK-style file-list assignment that applies before the next input file is consumed. > - Use `-v name=value` instead when the variable must exist before `BEGIN`. > - As in gawk, once the program text has been supplied (with `-f` or `-L`), an unknown option ends option processing and is passed on to the AWK program through `ARGV`, which is useful for `#!` interpreter scripts. +> - The value of a value-taking short option may be attached to the option letter (POSIX/getopt style): `-fprog.awk`, `-vx=1`, and `-F:` are equivalent to `-f prog.awk`, `-v x=1`, and `-F :`. This applies to `-f`, `-v`, `-F`, `-L`, `-K`, and `-l`. > > - Variables and formatting > diff --git a/src/test/java/io/jawk/CliOptionTest.java b/src/test/java/io/jawk/CliOptionTest.java index 9061f856..fcd28ddb 100644 --- a/src/test/java/io/jawk/CliOptionTest.java +++ b/src/test/java/io/jawk/CliOptionTest.java @@ -169,6 +169,72 @@ public void profileOptionWithEmptyFilenameIsRejected() throws Exception { assertTrue(result.thrownException().getMessage().contains("Need output filename for --profile")); } + @Test + public void attachedProgramFileOptionLoadsScript() throws Exception { + AwkTestSupport + .cliTest("CLI -fprog.awk loads the script like -f prog.awk") + .file("prog.awk", "BEGIN { print \"attached\" }") + .argument("-f{{prog.awk}}") + .expectLines("attached") + .runAndAssert(); + } + + @Test + public void attachedProgramFileOptionCombinesWithSeparateForm() throws Exception { + AwkTestSupport + .cliTest("CLI mixes -fone.awk with -f two.awk") + .file("one.awk", "BEGIN { print \"one\" }") + .file("two.awk", "BEGIN { print \"two\" }") + .argument("-f{{one.awk}}", "-f", "{{two.awk}}") + .expectLines("one", "two") + .runAndAssert(); + } + + @Test + public void attachedVariableAssignmentIsApplied() throws Exception { + AwkTestSupport + .cliTest("CLI -vx=42 assigns the variable like -v x=42") + .argument("-vx=42") + .script("BEGIN { print x }") + .expectLines("42") + .runAndAssert(); + } + + @Test + public void attachedFieldSeparatorIsApplied() throws Exception { + AwkTestSupport + .cliTest("CLI -F: sets the field separator like -F :") + .argument("-F:") + .script("{ print $2 }") + .stdin("a:b:c\n") + .expectLines("b") + .runAndAssert(); + } + + @Test + public void attachedOptionArgumentAfterDoubleDashStaysInArgv() throws Exception { + AwkTestSupport + .cliTest("CLI attached option-argument after -- is an operand") + .file("argv.awk", "BEGIN { for (i = 1; i < ARGC; i++) print i \"=\" ARGV[i] }") + .argument("-f", "{{argv.awk}}", "--") + .operand("-fnot-an-option.awk") + .expectLines("1=-fnot-an-option.awk") + .runAndAssert(); + } + + @Test + public void attachedUnknownOptionWithoutScriptIsStillRejected() throws Exception { + AwkTestSupport.TestResult result = AwkTestSupport + .cliTest("CLI unknown glued option without script is rejected") + .argument("-q2") + .script("{ print }") + .expectThrow(IllegalArgumentException.class) + .run(); + + result.assertExpected(); + assertTrue(result.thrownException().getMessage().contains("Unknown parameter: -q2")); + } + @Test public void doubleDashEndsOptionProcessing() throws Exception { // After "--", the dash-leading argument is no longer an option: it is