Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,17 @@

Jawk is a pure Java implementation of [AWK](https://en.wikipedia.org/wiki/AWK). You can run it as a CLI, embed it directly in Java applications, compile scripts to reusable tuples, evaluate AWK expressions, feed it structured input, load extensions explicitly, and enable a sandboxed runtime when you need tighter execution constraints.

Gawk-specific builtins — `asort()`, `asorti()`, `typeof()`, `isarray()`, `mkbool()`, `gensub()`, and `PROCINFO["sorted_in"]`-controlled array traversal — are available by default through the built-in GNU Awk compatibility extension.
## Support for POSIX AWK and Gawk

Jawk fully implements POSIX AWK, and adds support for the most commonly used gawk-specific features:

- Builtins, available by default through the built-in GNU Awk compatibility extension: `asort()`, `asorti()`, `typeof()`, `isarray()`, `mkbool()`, `gensub()`, and `PROCINFO["sorted_in"]`-controlled array traversal
- Arrays of arrays (`a[i][j]`) and typed regexp literals (`@/re/`)
- `BEGINFILE` / `ENDFILE` special patterns, with the `ERRNO` and `ARGIND` special variables, so a script can hook into the command-line file processing loop and skip unreadable files without a fatal error
- The `nextfile` statement
- The `IGNORECASE`, `SYMTAB`, and `FUNCTAB` special variables

As in gawk, the gawk-specific syntax is not special in `--posix` mode.

## CLI Example

Expand Down
167 changes: 164 additions & 3 deletions src/it/java/io/jawk/gawk/GawkExtensionIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@
* ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱
*/

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import io.jawk.AwkTestSupport;
import org.junit.Test;

Expand Down Expand Up @@ -1553,12 +1559,167 @@ public void test_mixed1() throws Exception {

@Test
public void test_beginfile1() throws Exception {
skip("BEGINFILE and ENDFILE are not implemented by Jawk yet.");
// Upstream: $AWK -f beginfile1.awk "$(srcdir)"/beginfile1.awk . ./no/such/file Makefile
// The script strips directories from FILENAME with gsub(/.*[/]/, ""),
// so operands are passed with forward slashes. "Makefile" is not part
// of the vendored fixtures (it is generated by gawk's configure), so
// an equivalent two-record file is materialized on the fly.
Path makefile = AwkTestSupport.sharedTempDirectory().resolve("Makefile");
Files.write(makefile, "# first record\n# second record\n".getBytes(StandardCharsets.UTF_8));
AwkTestSupport
.cliTest("GAWK beginfile1")
.argument("-f", gawkFile("beginfile1.awk"))
.operand(
slashed(gawkPath("beginfile1.awk")),
".",
"./no/such/file",
slashed(makefile))
.expectLines(gawkPath("beginfile1.ok"))
.expectExit(0)
.runAndAssert();
}

// The gawk beginfile2 target is a shell script running 16 sub-tests; the
// reproducible ones are transcribed below as test_beginfile2_test*. The
// remaining sub-tests rely on non-redirected getline in BEGIN/END driving
// the BEGINFILE/ENDFILE hooks mid-statement, or compare non-zero gawk CLI
// transcripts, which Jawk does not reproduce.

@Test
public void test_beginfile2_test2() throws Exception {
AwkTestSupport
.cliTest("GAWK beginfile2 --Test 2--")
.argument(beginfile2Program(2))
.operand(slashed(gawkPath("beginfile2.in")), "file/does/not/exist")
.postProcessWith(GawkExtensionIT::stripGawkDirectory)
.expectLines(beginfile2ExpectedLines("2"))
.expectExit(0)
.runAndAssert();
}

@Test
public void test_beginfile2() throws Exception {
skip("BEGINFILE and ENDFILE are not implemented by Jawk yet.");
public void test_beginfile2_test5() throws Exception {
AwkTestSupport
.cliTest("GAWK beginfile2 --Test 5--")
.argument(beginfile2Program(5))
.operand(slashed(gawkPath("beginfile2.in")))
.expectLines(beginfile2ExpectedLines("5"))
.expectExit(0)
.runAndAssert();
}

@Test
public void test_beginfile2_test9a() throws Exception {
AwkTestSupport
.cliTest("GAWK beginfile2 --Test 9a--")
.argument(beginfile2Program(9))
.operand("file/does/not/exist", slashed(gawkPath("beginfile2.in")))
.postProcessWith(GawkExtensionIT::stripGawkDirectory)
.expectLines(beginfile2ExpectedLines("9a"))
.expectExit(0)
.runAndAssert();
}

@Test
public void test_beginfile2_test9b() throws Exception {
AwkTestSupport
.cliTest("GAWK beginfile2 --Test 9b--")
.preassign("skip", 1)
.argument(beginfile2Program(9))
.operand("file/does/not/exist", slashed(gawkPath("beginfile2.in")))
.postProcessWith(GawkExtensionIT::stripGawkDirectory)
.expectLines(beginfile2ExpectedLines("9b"))
.expectExit(0)
.runAndAssert();
}

@Test
public void test_beginfile2_test12() throws Exception {
AwkTestSupport
.cliTest("GAWK beginfile2 --Test 12--")
.argument(beginfile2Program(12))
.operand(slashed(gawkPath("beginfile2.in")), slashed(gawkPath("beginfile2.sh")))
.postProcessWith(GawkExtensionIT::stripGawkDirectory)
.expectLines(beginfile2ExpectedLines("12"))
.expectExit(0)
.runAndAssert();
}

@Test
public void test_beginfile2_test15() throws Exception {
AwkTestSupport
.cliTest("GAWK beginfile2 --Test 15--")
.argument(beginfile2Program(15))
.operand(slashed(gawkPath("beginfile2.in")), slashed(gawkPath("beginfile2.sh")))
.postProcessWith(GawkExtensionIT::stripGawkDirectory)
.expectLines(beginfile2ExpectedLines("15"))
.expectExit(0)
.runAndAssert();
}

/**
* Extracts one sub-test program from the vendored {@code beginfile2.in},
* exactly like {@code beginfile2.sh} does with its awk range patterns.
* The {@code #TESTn#} marker lines are comments and stay harmless.
*
* @param testNumber sub-test number
* @return the AWK program of that sub-test
*/
private static String beginfile2Program(int testNumber) throws IOException {
String text = gawkText("beginfile2.in");
String beginMarker = "#TEST" + testNumber + "#";
String endMarker = "#TEST" + (testNumber + 1) + "#";
return text.substring(text.indexOf(beginMarker), text.indexOf(endMarker) + endMarker.length());
}

/**
* Extracts the expected transcript of one beginfile2 sub-test from the
* vendored {@code beginfile2.ok}, i.e. the lines between the
* {@code --Test N--} banner and the next banner.
*
* @param testLabel sub-test label as it appears in the banner (e.g. "9a")
* @return the expected output lines of that sub-test
*/
private static List<String> beginfile2ExpectedLines(String testLabel) throws IOException {
List<String> lines = Files.readAllLines(gawkPath("beginfile2.ok"), StandardCharsets.UTF_8);
List<String> section = new ArrayList<>();
boolean inSection = false;
for (String line : lines) {
if (line.startsWith("--Test ")) {
if (inSection) {
break;
}
inSection = line.equals("--Test " + testLabel + "--");
continue;
}
if (inSection) {
section.add(line);
}
}
return section;
}

/**
* Removes the vendored gawk directory prefix from FILENAME occurrences in
* the transcript, so it can be compared with the {@code .ok} files that
* gawk produces while running inside that directory.
*
* @param output raw transcript
* @return transcript with bare fixture file names
*/
private static String stripGawkDirectory(String output) {
return output.replace(slashed(GAWK_DIRECTORY) + "/", "");
}

/**
* Returns the path as a string with forward slashes, so FILENAME-based
* basename extraction with {@code gsub(/.*[/]/, "")} works on Windows too.
*
* @param path path to convert
* @return the path with forward slashes
*/
private static String slashed(Path path) {
return path.toString().replace('\\', '/');
}

@Test
Expand Down
5 changes: 4 additions & 1 deletion src/it/java/io/jawk/gawk/GawkOptionalFeatureIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -729,7 +729,10 @@ public void test_testext() throws Exception {

@Test
public void test_getfile() throws Exception {
skip(MANUAL_SKIP_REASON);
skip(
"Requires gawk's dynamic extension API (the get_file() input hand-off used by gawkextlib),"
+ " which Jawk does not provide. The BEGINFILE/ENDFILE rules the fixture also exercises"
+ " are covered by GawkExtensionIT.test_beginfile1 and test_beginfile2_test*.");
}

@Test
Expand Down
5 changes: 4 additions & 1 deletion src/main/java/io/jawk/Cli.java
Original file line number Diff line number Diff line change
Expand Up @@ -645,7 +645,10 @@ private static void usage(PrintStream dest) {
.println(
" -S, --sandbox = (extension) Enable sandbox mode (no system(), redirection, pipelines, or"
+ " dynamic extensions).");
dest.println(" --posix = Enforce POSIX-compatible behavior such as disabling nested arrays.");
dest
.println(
" --posix = Enforce POSIX-compatible behavior such as disabling nested arrays and"
+ " BEGINFILE/ENDFILE rules.");
dest.println(" --dump-syntax = Print the syntax tree.");
dest.println(" --dump-intermediate = Print the intermediate code.");
dest.println(" -s, --no-optimize = (extension) Disable optimizations during compilation.");
Expand Down
Loading
Loading