Skip to content

Commit

Permalink
New Java based Nessie CLI tool + REPL
Browse files Browse the repository at this point in the history
Features & functionalities:

* SQL-ish syntax.
* REPL.
* Built-in online `HELP` command (and `HELP <command>` command ;), that also shows the commands' syntaxes in the online help. The same information is also published on the projectnessie.org site.
* Auto-completion of commands, keywords and reference names (use TAB).
* Syntax highlighting.
* Paging of long results - showing a lot of content keys or a long commit log just works like `less` on your Linux or macOS box.
* Command history, persisted in your home directory (can be turned off).
* "Just" run a one or more Nessie REPL commands - pass those as arguments on the command line or as a script.
* Supports all authentication mechanisms that the Nessie Java supports, including all OAuth2 flows.
* Relatively small, currently just ~14.5MB.
* Available commands to manage branches and tags, drop tables and views, manage namespaces, list and show tables & views, merge and help.
* Contains interactive functionality that content-generator tool and pynessie provide, but not potentially dangerous operations.

It's not based on Quarkus, but "plain" Java 11, allows a smaller uber-jar of 14.5 instead of 18.5MB, also improves startup time a bit.

Also included in this PR:

* Updates to the web site, syntax docs generated from Grammar.
* Same help texts used for online help + web site.
* Blog post.

Uses congocc Grammar and parser/lexer, really quick parsing and no runtime dependencies required. congocc also allows relatively(!) easy integration of command line completion and syntax highlighting.

To try the Nessie CLI tool: `./gradlew :nessie-cli:jar && java -jar cli/cli/build/libs/nessie-cli-0.80.1-SNAPSHOT.jar`
  • Loading branch information
snazy committed Apr 29, 2024
1 parent 7010ba1 commit 34dec4e
Show file tree
Hide file tree
Showing 114 changed files with 9,829 additions and 62 deletions.
4 changes: 2 additions & 2 deletions api/model-quarkus/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ import io.smallrye.openapi.gradleplugin.SmallryeOpenApiTask
import org.apache.tools.ant.filters.ReplaceTokens

plugins {
id("nessie-conventions-quarkus")
id("nessie-conventions-server")
id("nessie-jacoco")
alias(libs.plugins.smallrye.openapi)
}

extra["maven.name"] = "Nessie - Model - Variant only for Java 17+ consumers"
extra["maven.name"] = "Nessie - Model - Variant only for Java 11+ consumers"

description =
"nessie-model-jakarta is effectively the same as nessie-model, but it is _not_ a " +
Expand Down
2 changes: 2 additions & 0 deletions bom/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ dependencies {
constraints {
api(rootProject)
api(project(":nessie-azurite-testcontainer"))
api(project(":nessie-cli"))
api(project(":nessie-cli-grammar"))
api(project(":nessie-client"))
api(project(":nessie-client-testextension"))
api(project(":nessie-combined-cs"))
Expand Down
81 changes: 81 additions & 0 deletions cli/cli/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright (C) 2022 Dremio
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import org.apache.tools.ant.taskdefs.condition.Os

plugins {
id("nessie-conventions-server")
id("nessie-jacoco")
id("nessie-shadow-jar")
}

extra["maven.name"] = "Nessie - CLI"

configurations.all { exclude(group = "org.projectnessie.nessie", module = "nessie-model") }

dependencies {
implementation(project(":nessie-model-quarkus"))
implementation(project(":nessie-client"))
implementation(project(":nessie-cli-grammar"))

implementation(libs.jline)
implementation(libs.picocli)

compileOnly(libs.immutables.value.annotations)
annotationProcessor(libs.immutables.value.processor)

compileOnly(libs.jakarta.annotation.api)
compileOnly(libs.microprofile.openapi)

implementation(platform(libs.jackson.bom))
implementation("com.fasterxml.jackson.core:jackson-databind")

compileOnly(libs.immutables.builder)
compileOnly(libs.immutables.value.annotations)
annotationProcessor(libs.immutables.value.processor)

runtimeOnly(libs.logback.classic)

testFixturesApi(libs.microprofile.openapi)

testFixturesApi(platform(libs.junit.bom))
testFixturesApi(libs.bundles.junit.testing)

testImplementation(project(":nessie-jaxrs-testextension"))

testImplementation(project(":nessie-versioned-storage-inmemory-tests"))

testCompileOnly(libs.immutables.value.annotations)
}

tasks.withType<ProcessResources>().configureEach {
from("src/main/resources") { duplicatesStrategy = DuplicatesStrategy.INCLUDE }
}

tasks.named<ShadowJar>("shadowJar").configure {
manifest { attributes["Main-Class"] = "org.projectnessie.nessie.cli.cli.NessieCliMain" }
}

// Testcontainers is not supported on Windows :(
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
tasks.named<Test>("intTest").configure { this.enabled = false }
}

// Issue w/ testcontainers/podman in GH workflows :(
if (Os.isFamily(Os.FAMILY_MAC) && System.getenv("CI") != null) {
tasks.named<Test>("intTest").configure { this.enabled = false }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* Copyright (C) 2024 Dremio
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.projectnessie.nessie.cli.cli;

import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.requireNonNull;

import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.UncheckedIOException;
import java.net.URL;
import java.util.Optional;
import org.jline.terminal.Terminal;
import org.jline.utils.AttributedStringBuilder;
import org.jline.utils.AttributedStyle;
import org.projectnessie.client.api.NessieApiV2;
import org.projectnessie.model.Reference;

public abstract class BaseNessieCli {

public static final AttributedStyle STYLE_ERROR = AttributedStyle.DEFAULT.foreground(128, 0, 0);
public static final AttributedStyle STYLE_FAINT = AttributedStyle.DEFAULT.faint();
public static final AttributedStyle STYLE_YELLOW =
AttributedStyle.DEFAULT.foreground(128, 128, 0);
public static final AttributedStyle STYLE_GREEN = AttributedStyle.DEFAULT.foreground(0, 128, 0);
public static final AttributedStyle STYLE_BLUE = AttributedStyle.DEFAULT.foreground(0, 0, 128);

private Integer exitWithCode;
private NessieApiV2 nessieApi;
private Reference currentReference;
private Terminal terminal;

public Integer exitWithCode() {
return exitWithCode;
}

public void exitWithCode(int exitCode) {
exitWithCode = exitCode;
}

public void setTerminal(Terminal terminal) {
this.terminal = terminal;
}

public PrintWriter writer() {
return terminal.writer();
}

public void exitRepl(int exitCode) {
exitWithCode = exitCode;
}

public Terminal terminal() {
return terminal;
}

public boolean dumbTerminal() {
return terminal.getType().equals("dumb");
}

public void setCurrentReference(Reference currentReference) {
this.currentReference = currentReference;
}

public Reference getCurrentReference() {
if (nessieApi == null) {
throw new NotConnectedException();
}
return currentReference;
}

public void connected(NessieApiV2 nessieApi) {
if (this.nessieApi != null) {
try {
this.nessieApi.close();
} catch (Exception e) {
@SuppressWarnings("resource")
PrintWriter writer = writer();
writer.println(
new AttributedStringBuilder()
.append("Failed to close the existing client: ")
.append(e.toString(), STYLE_ERROR));
}
}
this.nessieApi = nessieApi;
}

public Optional<NessieApiV2> nessieApi() {
return Optional.ofNullable(nessieApi);
}

public NessieApiV2 mandatoryNessieApi() {
if (nessieApi == null) {
throw new NotConnectedException();
}
return nessieApi;
}

public String readResource(String resource) {
URL url = NessieCliImpl.class.getResource(resource);
try (InputStream in =
requireNonNull(url, "Could not open resource from classpath " + resource)
.openConnection()
.getInputStream()) {
return new String(in.readAllBytes(), UTF_8);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright (C) 2024 Dremio
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.projectnessie.nessie.cli.cli;

/**
* "Marker" exception to tell the CLI/REPL that the command execution failed, but the error was
* already handled.
*/
public class CliCommandFailedException extends RuntimeException {
public CliCommandFailedException() {
super("Previous command failed.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Copyright (C) 2024 Dremio
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.projectnessie.nessie.cli.cli;

import static org.projectnessie.nessie.cli.cli.NessieCliImpl.OPTION_COMMAND;
import static org.projectnessie.nessie.cli.cli.NessieCliImpl.OPTION_CONTINUE_ON_ERROR;
import static org.projectnessie.nessie.cli.cli.NessieCliImpl.OPTION_KEEP_RUNNING;
import static org.projectnessie.nessie.cli.cli.NessieCliImpl.OPTION_SCRIPT;

import java.util.List;
import picocli.CommandLine.ArgGroup;
import picocli.CommandLine.Option;

class CommandsToRun {

@ArgGroup CommandsSource commandsSource;

@Option(
names = {"-K", OPTION_KEEP_RUNNING},
description = {
"When running commands via the "
+ OPTION_COMMAND
+ " or "
+ OPTION_SCRIPT
+ " option the process will exit once the commands have been executed.",
"To keep the REPL running, specify this option."
+ "See the "
+ OPTION_CONTINUE_ON_ERROR
+ " option."
})
boolean keepRunning;

@Option(
names = {"-E", OPTION_CONTINUE_ON_ERROR},
description = {
"When running commands via the "
+ OPTION_COMMAND
+ " or "
+ OPTION_SCRIPT
+ " option the process will stop/exit when a command could not be parsed or ran into an error.",
"Specifying this option lets the REPL continue executing the remaining commands after parse or runtime errors."
})
boolean continueOnError;

@Override
public String toString() {
return "CommandsToRun{"
+ "commandsSource="
+ commandsSource
+ ", keepRunning="
+ keepRunning
+ ", continueOnError="
+ continueOnError
+ '}';
}

static class CommandsSource {
@Option(
names = {"-s", OPTION_SCRIPT},
description = {
"Run the commands in the Nessie CLI script referenced by this option.",
"Possible values are either a file path or use the minus character ('-') to read the script from stdin."
})
String scriptFile;

@Option(
names = {"-c", OPTION_COMMAND},
arity = "*",
description = {
"Nessie CLI commands to run. Each value represents one command.",
"The process will exit once all specified commands have been executed. "
+ "To keep the REPL running in case of errors, specify the "
+ OPTION_KEEP_RUNNING
+ " option."
})
List<String> commands = List.of();

@Override
public String toString() {
return "CommandsSource{" + "runScript='" + scriptFile + '\'' + ", commands=" + commands + '}';
}
}
}
Loading

0 comments on commit 34dec4e

Please sign in to comment.