Skip to content

Commit

Permalink
A simple picocli/native-image ion-java CLI
Browse files Browse the repository at this point in the history
- Supports formatted transcription of input files or stdin to a
  destination file or stdout

# Conflicts:
#	ion-java-cli/src/main/java/com/amazon/tools/cli/OutputFormat.java
  • Loading branch information
jobarr-amzn committed Dec 12, 2024
1 parent 7917c59 commit 9cc5e92
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 1 deletion.
27 changes: 27 additions & 0 deletions ion-java-cli/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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."
Expand All @@ -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<JavaCompile> {
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")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
113 changes: 113 additions & 0 deletions ion-java-cli/src/main/java/com/amazon/tools/cli/SimpleIonCli.java
Original file line number Diff line number Diff line change
@@ -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 = "<format>",
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 extends Throwable> 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);
}
}

0 comments on commit 9cc5e92

Please sign in to comment.