Skip to content

Commit ed75e47

Browse files
committed
A simple picocli/native-image ion-java CLI
- Supports formatted transcription of input files or stdin to a destination file or stdout
1 parent d434d2a commit ed75e47

File tree

3 files changed

+142
-2
lines changed

3 files changed

+142
-2
lines changed

ion-java-cli/build.gradle.kts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
plugins {
22
java
33
application
4+
// Apply GraalVM Native Image plugin
5+
id("org.graalvm.buildtools.native") version "0.10.3"
46
}
57

68
description = "A CLI that implements the standard interface defined by ion-test-driver."
@@ -15,8 +17,33 @@ repositories {
1517
dependencies {
1618
implementation("args4j:args4j:2.33")
1719
implementation(rootProject)
20+
21+
implementation("info.picocli:picocli:4.7.6")
22+
annotationProcessor("info.picocli:picocli-codegen:4.7.6")
23+
}
24+
25+
tasks.withType<JavaCompile> {
26+
options.compilerArgs.add("-Aproject=${project.group}/${project.name}")
1827
}
1928

2029
application {
2130
mainClass.set("com.amazon.tools.cli.IonJavaCli")
2231
}
32+
33+
// Defines an ion-java-cli:nativeCompile task which produces ion-java-cli/build/native/nativeCompile/jion
34+
// You need to have GRAALVM_HOME pointed at a GraalVM installation
35+
// You can get one of those via e.g. `sdk install java 17.0.9-graalce`
36+
// See: https://sdkman.io/
37+
graalvmNative {
38+
testSupport.set(false)
39+
binaries {
40+
named("main") {
41+
imageName.set("jion")
42+
mainClass.set("com.amazon.tools.cli.SimpleIonCli")
43+
buildArgs.add("-O4")
44+
}
45+
}
46+
binaries.all {
47+
buildArgs.add("--verbose")
48+
}
49+
}

ion-java-cli/src/main/java/com/amazon/tools/cli/OutputFormat.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,12 @@ public IonWriter createIonWriterWithImports(OutputStream outputStream, SymbolTab
7474
@Override
7575
public IonWriter createIonWriter(OutputStream outputStream) {
7676
NoOpOutputStream out = new NoOpOutputStream();
77-
return IonTextWriterBuilder.pretty().build(out);
77+
return IonTextWriterBuilder.standard().build(out);
7878
}
7979

8080
@Override
8181
public IonWriter createIonWriterWithImports(OutputStream outputStream, SymbolTable[] imports) {
82-
return IonTextWriterBuilder.pretty().withImports(imports).build(outputStream);
82+
return IonTextWriterBuilder.standard().withImports(imports).build(outputStream);
8383
}
8484
};
8585

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package com.amazon.tools.cli;
2+
3+
4+
import com.amazon.ion.IonReader;
5+
import com.amazon.ion.IonWriter;
6+
import com.amazon.ion.system.IonReaderBuilder;
7+
import picocli.CommandLine;
8+
import picocli.CommandLine.Command;
9+
import picocli.CommandLine.HelpCommand;
10+
import picocli.CommandLine.Option;
11+
import picocli.CommandLine.Parameters;
12+
13+
import java.io.ByteArrayInputStream;
14+
import java.io.File;
15+
import java.io.FileDescriptor;
16+
import java.io.FileInputStream;
17+
import java.io.FileNotFoundException;
18+
import java.io.FileOutputStream;
19+
import java.io.IOException;
20+
import java.io.InputStream;
21+
import java.io.OutputStream;
22+
import java.io.SequenceInputStream;
23+
import java.util.Arrays;
24+
25+
@Command(
26+
name = SimpleIonCli.NAME,
27+
version = SimpleIonCli.VERSION,
28+
subcommands = {HelpCommand.class},
29+
mixinStandardHelpOptions = true
30+
)
31+
class SimpleIonCli {
32+
33+
public static final String NAME = "jion";
34+
public static final String VERSION = "2024-10-31";
35+
//TODO: Replace with InputStream.nullInputStream in JDK 11+
36+
public static final InputStream EMPTY = new ByteArrayInputStream(new byte[0]);
37+
38+
public static void main(String[] args) {
39+
CommandLine commandLine = new CommandLine(new SimpleIonCli())
40+
.setCaseInsensitiveEnumValuesAllowed(true)
41+
.setUsageHelpAutoWidth(true);
42+
System.exit(commandLine.execute(args));
43+
}
44+
45+
@Option(names={"-f", "--format", "--output-format"}, defaultValue = "pretty",
46+
description = "Output format, from the set (text | pretty | binary | none).",
47+
paramLabel = "<format>",
48+
scope = CommandLine.ScopeType.INHERIT)
49+
OutputFormat outputFormat;
50+
51+
@Option(names={"-o", "--output"}, paramLabel = "FILE", description = "Output file",
52+
scope = CommandLine.ScopeType.INHERIT)
53+
File outputFile;
54+
55+
@Command(name = "cat", aliases = {"process"},
56+
description = "concatenate FILE(s) in the requested Ion output format",
57+
mixinStandardHelpOptions = true)
58+
int cat( @Parameters(paramLabel = "FILE") File... files) {
59+
60+
if (outputFormat == OutputFormat.EVENTS) {
61+
System.err.println("'events' output format is not supported");
62+
return CommandLine.ExitCode.USAGE;
63+
}
64+
65+
//TODO: This is not resilient to problems with a single file. Should it be?
66+
try (InputStream in = getInputStream(files);
67+
IonReader reader = IonReaderBuilder.standard().build(in);
68+
OutputStream out = getOutputStream(outputFile);
69+
IonWriter writer = outputFormat.createIonWriter(out)) {
70+
// getInputStream will look for stdin if we don't supply
71+
writer.writeValues(reader);
72+
} catch (IOException e) {
73+
System.err.println(e.getMessage());
74+
return CommandLine.ExitCode.SOFTWARE;
75+
}
76+
77+
// process files
78+
return CommandLine.ExitCode.OK;
79+
}
80+
81+
private static InputStream getInputStream(File... files) {
82+
if (files == null || files.length == 0) return new FileInputStream(FileDescriptor.in);
83+
84+
// As convenient as this formulation is I'm not sure of the ordering guarantees here
85+
// Revisit if that is ever problematic
86+
return Arrays.stream(files)
87+
.map(SimpleIonCli::getInputStream)
88+
.reduce(EMPTY, SequenceInputStream::new);
89+
}
90+
91+
private static InputStream getInputStream(File inputFile) {
92+
try {
93+
return new FileInputStream(inputFile);
94+
} catch (FileNotFoundException e) {
95+
throw cloak(e);
96+
}
97+
}
98+
99+
// Removing some boilerplate from checked-exception consuming paths, without RuntimeException wrapping
100+
// JLS Section 18.4 covers type inference for generic methods,
101+
// including the rule that `throws T` is inferred as RuntimeException if possible.
102+
// See e.g. https://www.rainerhahnekamp.com/en/ignoring-exceptions-in-java/
103+
private static <T extends Throwable> T cloak(Throwable t) throws T {
104+
@SuppressWarnings("unchecked")
105+
T result = (T) t;
106+
return result;
107+
}
108+
109+
private static FileOutputStream getOutputStream(File outputFile) throws IOException {
110+
// non-line-buffered stdout, or the requested file output
111+
return outputFile == null ? new FileOutputStream(FileDescriptor.out) : new FileOutputStream(outputFile);
112+
}
113+
}

0 commit comments

Comments
 (0)