Skip to content

Commit b59bce3

Browse files
authored
feat: Add shell mode for autocompletion of commands. (#118)
1 parent ea07415 commit b59bce3

3 files changed

Lines changed: 103 additions & 0 deletions

File tree

ice/pom.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,16 @@
512512
<artifactId>picocli</artifactId>
513513
<version>${picocli.version}</version>
514514
</dependency>
515+
<dependency>
516+
<groupId>info.picocli</groupId>
517+
<artifactId>picocli-shell-jline3</artifactId>
518+
<version>${picocli.version}</version>
519+
</dependency>
520+
<dependency>
521+
<groupId>org.jline</groupId>
522+
<artifactId>jline</artifactId>
523+
<version>${jline.version}</version>
524+
</dependency>
515525
<!--jackson-->
516526
<dependency>
517527
<groupId>com.fasterxml.jackson.core</groupId>

ice/src/main/java/com/altinity/ice/cli/Main.java

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,23 +37,36 @@
3737
import io.prometheus.metrics.instrumentation.jvm.JvmMetrics;
3838
import java.io.IOException;
3939
import java.net.URISyntaxException;
40+
import java.nio.file.Path;
4041
import java.util.ArrayList;
4142
import java.util.Arrays;
4243
import java.util.Base64;
4344
import java.util.HashSet;
4445
import java.util.List;
4546
import java.util.Scanner;
47+
import java.util.function.Supplier;
4648
import java.util.stream.Collectors;
4749
import org.apache.curator.shaded.com.google.common.net.HostAndPort;
4850
import org.apache.iceberg.CatalogProperties;
4951
import org.apache.iceberg.catalog.Namespace;
5052
import org.apache.iceberg.catalog.TableIdentifier;
5153
import org.apache.iceberg.rest.RESTCatalog;
5254
import org.eclipse.jetty.server.Server;
55+
import org.jline.console.SystemRegistry;
56+
import org.jline.console.impl.SystemRegistryImpl;
57+
import org.jline.reader.EndOfFileException;
58+
import org.jline.reader.LineReader;
59+
import org.jline.reader.LineReaderBuilder;
60+
import org.jline.reader.Parser;
61+
import org.jline.reader.UserInterruptException;
62+
import org.jline.reader.impl.DefaultParser;
63+
import org.jline.terminal.Terminal;
64+
import org.jline.terminal.TerminalBuilder;
5365
import org.slf4j.Logger;
5466
import org.slf4j.LoggerFactory;
5567
import picocli.AutoComplete;
5668
import picocli.CommandLine;
69+
import picocli.shell.jline3.PicocliCommands;
5770

5871
@CommandLine.Command(
5972
name = "ice",
@@ -66,6 +79,8 @@ public final class Main {
6679

6780
private static final Logger logger = LoggerFactory.getLogger(Main.class);
6881

82+
private boolean inShellMode = false;
83+
6984
@CommandLine.Option(
7085
names = {"-c", "--config"},
7186
description = "/path/to/config.yaml ($CWD/.ice.yaml by default)",
@@ -835,6 +850,83 @@ private RESTCatalog loadCatalog(String configFile) throws IOException {
835850
return catalog;
836851
}
837852

853+
@CommandLine.Command(
854+
name = "shell",
855+
description = "Start interactive shell with tab completion.",
856+
mixinStandardHelpOptions = true)
857+
void shell() throws IOException {
858+
if (inShellMode) {
859+
logger.warn("Already in shell mode");
860+
return;
861+
}
862+
inShellMode = true;
863+
864+
final String savedConfigFile = this.configFile;
865+
final String savedLogLevel = this.logLevel;
866+
final boolean savedInsecure = this.insecure;
867+
868+
Supplier<Path> workDir = () -> Path.of(System.getProperty("user.dir"));
869+
870+
CommandLine cmd = new CommandLine(this);
871+
cmd.getSubcommands().remove("shell");
872+
873+
cmd.setExecutionStrategy(
874+
parseResult -> {
875+
Main main = (Main) parseResult.commandSpec().root().userObject();
876+
if (!parseResult.hasMatchedOption("--config")) {
877+
main.configFile = savedConfigFile;
878+
}
879+
if (!parseResult.hasMatchedOption("--log-level")) {
880+
main.logLevel = savedLogLevel;
881+
}
882+
if (!parseResult.hasMatchedOption("--insecure")) {
883+
main.insecure = savedInsecure;
884+
}
885+
ch.qos.logback.classic.Logger rootLogger =
886+
(ch.qos.logback.classic.Logger)
887+
LoggerFactory.getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME);
888+
rootLogger.setLevel(Level.toLevel(main.logLevel.toUpperCase(), Level.INFO));
889+
return new CommandLine.RunLast().execute(parseResult);
890+
});
891+
cmd.setExecutionExceptionHandler(
892+
(Exception ex, CommandLine self, CommandLine.ParseResult res) -> {
893+
logger.error("Error", ex);
894+
return 1;
895+
});
896+
897+
PicocliCommands picocliCommands = new PicocliCommands(cmd);
898+
899+
try (Terminal terminal = TerminalBuilder.builder().build()) {
900+
Parser parser = new DefaultParser();
901+
SystemRegistry systemRegistry = new SystemRegistryImpl(parser, terminal, workDir, null);
902+
systemRegistry.setCommandRegistries(picocliCommands);
903+
904+
LineReader reader =
905+
LineReaderBuilder.builder()
906+
.terminal(terminal)
907+
.completer(systemRegistry.completer())
908+
.parser(parser)
909+
.variable(LineReader.LIST_MAX, 50)
910+
.build();
911+
912+
String prompt = "ice> ";
913+
914+
while (true) {
915+
try {
916+
systemRegistry.cleanUp();
917+
String line = reader.readLine(prompt);
918+
systemRegistry.execute(line);
919+
} catch (UserInterruptException e) {
920+
System.exit(0);
921+
} catch (EndOfFileException e) {
922+
return;
923+
} catch (Exception e) {
924+
systemRegistry.trace(e);
925+
}
926+
}
927+
}
928+
}
929+
838930
public static void main(String[] args) {
839931
CommandLine cmd = new CommandLine(new Main());
840932
CommandLine.IExecutionStrategy defaultExecutionStrategy = cmd.getExecutionStrategy();

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
<assertj.version>3.27.2</assertj.version>
3131
<!-- versions:transitive -->
3232
<caffeine.version>3.2.0</caffeine.version>
33+
<jline.version>3.26.1</jline.version>
3334
</properties>
3435

3536
<dependencyManagement>

0 commit comments

Comments
 (0)