diff --git a/ion-java-cli/build.gradle.kts b/ion-java-cli/build.gradle.kts index 8ddd311a4..a1c515aed 100644 --- a/ion-java-cli/build.gradle.kts +++ b/ion-java-cli/build.gradle.kts @@ -1,6 +1,8 @@ plugins { java application + // Apply GraalVM Native Image plugin + id("org.graalvm.buildtools.native") version "0.10.3" } description = "A CLI that implements the standard interface defined by ion-test-driver." @@ -15,8 +17,33 @@ repositories { dependencies { implementation("args4j:args4j:2.33") implementation(rootProject) + + implementation("info.picocli:picocli:4.7.6") + annotationProcessor("info.picocli:picocli-codegen:4.7.6") +} + +tasks.withType { + options.compilerArgs.add("-Aproject=${project.group}/${project.name}") } application { mainClass.set("com.amazon.tools.cli.IonJavaCli") } + +// Defines an ion-java-cli:nativeCompile task which produces ion-java-cli/build/native/nativeCompile/jion +// You need to have GRAALVM_HOME pointed at a GraalVM installation +// You can get one of those via e.g. `sdk install java 17.0.9-graalce` +// See: https://sdkman.io/ +graalvmNative { + testSupport.set(false) + binaries { + named("main") { + imageName.set("jion") + mainClass.set("com.amazon.tools.cli.SimpleIonCli") + buildArgs.add("-O4") + } + } + binaries.all { + buildArgs.add("--verbose") + } +} diff --git a/ion-java-cli/src/main/java/com/amazon/tools/cli/OutputFormat.java b/ion-java-cli/src/main/java/com/amazon/tools/cli/OutputFormat.java index ab1882934..3f1609bec 100644 --- a/ion-java-cli/src/main/java/com/amazon/tools/cli/OutputFormat.java +++ b/ion-java-cli/src/main/java/com/amazon/tools/cli/OutputFormat.java @@ -40,7 +40,7 @@ private static IonWriter createIonWriter(OutputFormat format, OutputStream out, switch (format) { case TEXT: return IonTextWriterBuilder.standard().withImports(symbols).build(out); case PRETTY: return IonTextWriterBuilder.pretty().withImports(symbols).build(out); - case EVENTS: return IonTextWriterBuilder.pretty().withImports(symbols).build(out); + case EVENTS: return IonTextWriterBuilder.standard().withImports(symbols).build(out); case BINARY: return IonBinaryWriterBuilder.standard().withImports(symbols).build(out); case NONE: return IonTextWriterBuilder.standard().withImports(symbols).build(new NoOpOutputStream()); default: throw new IllegalStateException("Unsupported output format: " + format); diff --git a/ion-java-cli/src/main/java/com/amazon/tools/cli/SimpleIonCli.java b/ion-java-cli/src/main/java/com/amazon/tools/cli/SimpleIonCli.java new file mode 100644 index 000000000..d6ddf6f53 --- /dev/null +++ b/ion-java-cli/src/main/java/com/amazon/tools/cli/SimpleIonCli.java @@ -0,0 +1,113 @@ +package com.amazon.tools.cli; + + +import com.amazon.ion.IonReader; +import com.amazon.ion.IonWriter; +import com.amazon.ion.system.IonReaderBuilder; +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.HelpCommand; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.SequenceInputStream; +import java.util.Arrays; + +@Command( + name = SimpleIonCli.NAME, + version = SimpleIonCli.VERSION, + subcommands = {HelpCommand.class}, + mixinStandardHelpOptions = true +) +class SimpleIonCli { + + public static final String NAME = "jion"; + public static final String VERSION = "2024-10-31"; + //TODO: Replace with InputStream.nullInputStream in JDK 11+ + public static final InputStream EMPTY = new ByteArrayInputStream(new byte[0]); + + public static void main(String[] args) { + CommandLine commandLine = new CommandLine(new SimpleIonCli()) + .setCaseInsensitiveEnumValuesAllowed(true) + .setUsageHelpAutoWidth(true); + System.exit(commandLine.execute(args)); + } + + @Option(names={"-f", "--format", "--output-format"}, defaultValue = "pretty", + description = "Output format, from the set (text | pretty | binary | none).", + paramLabel = "", + scope = CommandLine.ScopeType.INHERIT) + OutputFormat outputFormat; + + @Option(names={"-o", "--output"}, paramLabel = "FILE", description = "Output file", + scope = CommandLine.ScopeType.INHERIT) + File outputFile; + + @Command(name = "cat", aliases = {"process"}, + description = "concatenate FILE(s) in the requested Ion output format", + mixinStandardHelpOptions = true) + int cat( @Parameters(paramLabel = "FILE") File... files) { + + if (outputFormat == OutputFormat.EVENTS) { + System.err.println("'events' output format is not supported"); + return CommandLine.ExitCode.USAGE; + } + + //TODO: This is not resilient to problems with a single file. Should it be? + try (InputStream in = getInputStream(files); + IonReader reader = IonReaderBuilder.standard().build(in); + OutputStream out = getOutputStream(outputFile); + IonWriter writer = outputFormat.createIonWriter(out)) { + // getInputStream will look for stdin if we don't supply + writer.writeValues(reader); + } catch (IOException e) { + System.err.println(e.getMessage()); + return CommandLine.ExitCode.SOFTWARE; + } + + // process files + return CommandLine.ExitCode.OK; + } + + private static InputStream getInputStream(File... files) { + if (files == null || files.length == 0) return new FileInputStream(FileDescriptor.in); + + // As convenient as this formulation is I'm not sure of the ordering guarantees here + // Revisit if that is ever problematic + return Arrays.stream(files) + .map(SimpleIonCli::getInputStream) + .reduce(EMPTY, SequenceInputStream::new); + } + + private static InputStream getInputStream(File inputFile) { + try { + return new FileInputStream(inputFile); + } catch (FileNotFoundException e) { + throw cloak(e); + } + } + + // Removing some boilerplate from checked-exception consuming paths, without RuntimeException wrapping + // JLS Section 18.4 covers type inference for generic methods, + // including the rule that `throws T` is inferred as RuntimeException if possible. + // See e.g. https://www.rainerhahnekamp.com/en/ignoring-exceptions-in-java/ + private static T cloak(Throwable t) throws T { + @SuppressWarnings("unchecked") + T result = (T) t; + return result; + } + + private static FileOutputStream getOutputStream(File outputFile) throws IOException { + // non-line-buffered stdout, or the requested file output + return outputFile == null ? new FileOutputStream(FileDescriptor.out) : new FileOutputStream(outputFile); + } +}