commandAliases = new HashMap<>();
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ImportCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ImportCommand.java
new file mode 100644
index 0000000000..80189933ee
--- /dev/null
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ImportCommand.java
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.knox.gateway.shell.commands;
+
+import org.apache.groovy.groovysh.jline.GroovyEngine;
+import org.jline.terminal.Terminal;
+
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Manages Groovy imports in the current shell session.
+ *
+ * Usage:
+ * :import - lists active imports
+ * :import org.apache.knox.gateway.shell.KnoxSession - adds a single import
+ * :import org.apache.knox.gateway.shell.* - wildcard import
+ */
+public class ImportCommand extends AbstractKnoxShellCommand {
+
+ private static final String NAME = ":import";
+ private static final String SHORTCUT = ":i";
+ private static final String DESC = "Import a class into the namespace";
+ private static final String USAGE = "Usage: :import []\n"
+ + " :import - list active imports\n"
+ + " :import - add a new import\n"
+ + " :import .* - wildcard import";
+ private static final String HELP = USAGE;
+
+ private final Set activeImports = new LinkedHashSet<>();
+
+ public ImportCommand(GroovyEngine engine, Terminal terminal) {
+ super(engine, terminal, NAME, SHORTCUT, DESC, USAGE, HELP);
+ }
+
+ /**
+ * Registers a built-in import so it appears in the :import listing.
+ * Called from Shell during startup for each pre-loaded Knox import.
+ */
+ public void registerBuiltIn(String fqcn) {
+ activeImports.add(fqcn);
+ }
+
+ /**
+ * Returns an unmodifiable view of the currently active imports.
+ * Used by ShowCommand to display imports via :show imports.
+ */
+ public Set getActiveImports() {
+ return Collections.unmodifiableSet(activeImports);
+ }
+
+ @Override
+ public Object execute(List args) {
+ if (args == null || args.isEmpty()) {
+ // List mode
+ if (activeImports.isEmpty()) {
+ terminal.writer().println("No imports registered.");
+ } else {
+ terminal.writer().println("Active imports:");
+ activeImports.forEach(i -> terminal.writer().println(" import " + i));
+ }
+ terminal.writer().flush();
+ return null;
+ }
+
+ // Join all args in case user typed "import java.util. *" with spaces
+ String target = String.join("", args).trim();
+
+ // Strip leading "import " if the user typed ":import import java.util.List"
+ if (target.toLowerCase().startsWith("import ")) {
+ target = target.substring(7).trim();
+ }
+
+ // Basic validation: must contain a dot (package separator)
+ if (!target.contains(".")) {
+ terminal.writer().println("Invalid import: '" + target
+ + "'. Expected a fully-qualified class or package (e.g. java.util.List or java.util.*).");
+ terminal.writer().flush();
+ return null;
+ }
+
+ try {
+ engine.execute("import " + target);
+ activeImports.add(target);
+ terminal.writer().println("==> import " + target);
+ } catch (Exception e) {
+ terminal.writer().println("Failed to import '" + target + "': " + e.getMessage());
+ }
+
+ terminal.writer().flush();
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoadCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoadCommand.java
new file mode 100644
index 0000000000..982ed51346
--- /dev/null
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoadCommand.java
@@ -0,0 +1,148 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.knox.gateway.shell.commands;
+
+import org.apache.groovy.groovysh.jline.GroovyEngine;
+import org.jline.terminal.Terminal;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Loads a Groovy script file or URL into the shell and executes it.
+ * Matches the old Groovysh :load command behavior.
+ *
+ * Usage:
+ * :load /path/to/script.groovy
+ * :load ~/scripts/setup.groovy
+ * :load https://example.com/script.groovy
+ * . /path/to/script.groovy (alias)
+ */
+public class LoadCommand extends AbstractKnoxShellCommand {
+
+ private static final String NAME = ":load";
+ private static final String SHORTCUT = ".";
+ private static final String DESC = "Load a file or URL into the buffer";
+ private static final String USAGE = "Usage: :load \n"
+ + " :load /path/to/script.groovy\n"
+ + " :load ~/scripts/setup.groovy\n"
+ + " :load https://example.com/script.groovy\n"
+ + " . /path/to/script.groovy";
+ private static final String HELP = USAGE;
+
+ public LoadCommand(GroovyEngine engine, Terminal terminal) {
+ super(engine, terminal, NAME, SHORTCUT, DESC, USAGE, HELP);
+ }
+
+ @Override
+ public Object execute(List args) throws Exception {
+ if (args == null || args.isEmpty()) {
+ terminal.writer().println(USAGE);
+ terminal.writer().flush();
+ return null;
+ }
+
+ // Join args to support paths with spaces (e.g., :load /my path/script.groovy)
+ String location = String.join(" ", args).trim();
+
+ String script;
+ try {
+ script = readScript(location);
+ } catch (Exception e) {
+ terminal.writer().println("Failed to load '" + location + "': " + e.getMessage());
+ terminal.writer().flush();
+ return null;
+ }
+
+ if (script.isEmpty()) {
+ terminal.writer().println("Warning: '" + location + "' is empty, nothing to execute.");
+ terminal.writer().flush();
+ return null;
+ }
+
+ terminal.writer().println("Loading " + location + " ...");
+ terminal.writer().flush();
+
+ try {
+ Object result = engine.execute(script);
+ if (result != null) {
+ terminal.writer().println("==> " + result);
+ terminal.writer().flush();
+ }
+ return result;
+ } catch (Exception e) {
+ terminal.writer().println("Error executing script: " + e.getMessage());
+ terminal.writer().flush();
+ return null;
+ }
+ }
+
+ private String readScript(String location) throws IOException {
+ // Try as URL first (http://, https://, file://)
+ if (isUrl(location)) {
+ return readFromUrl(location);
+ }
+
+ // Expand ~ to user home
+ if (location.startsWith("~")) {
+ location = System.getProperty("user.home") + location.substring(1);
+ }
+
+ Path path = Paths.get(location);
+ if (!Files.exists(path)) {
+ throw new IOException("File not found: " + path.toAbsolutePath());
+ }
+ if (!Files.isReadable(path)) {
+ throw new IOException("File is not readable: " + path.toAbsolutePath());
+ }
+ if (Files.isDirectory(path)) {
+ throw new IOException("Path is a directory, not a file: " + path.toAbsolutePath());
+ }
+
+ return Files.readString(path);
+ }
+
+ private boolean isUrl(String location) {
+ return location.startsWith("http://")
+ || location.startsWith("https://")
+ || location.startsWith("file://");
+ }
+
+ private String readFromUrl(String urlStr) throws IOException {
+ URL url;
+ try {
+ url = new URL(urlStr);
+ } catch (MalformedURLException e) {
+ throw new IOException("Invalid URL: " + urlStr, e);
+ }
+
+ try (BufferedReader reader = new BufferedReader(
+ new InputStreamReader(url.openStream(), StandardCharsets.UTF_8))) {
+ return reader.lines().collect(Collectors.joining("\n"));
+ }
+ }
+}
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ShowCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ShowCommand.java
new file mode 100644
index 0000000000..d673c05e8e
--- /dev/null
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ShowCommand.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.knox.gateway.shell.commands;
+
+import org.apache.groovy.groovysh.jline.GroovyEngine;
+import org.jline.terminal.Terminal;
+
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Shows variables, classes or imports in the current shell session.
+ *
+ * Usage:
+ * :show - lists all variables (default)
+ * :show variables - lists all variables
+ * :show imports - lists active imports
+ * :show all - lists both variables and imports
+ */
+public class ShowCommand extends AbstractKnoxShellCommand {
+
+ private static final String NAME = ":show";
+ private static final String SHORTCUT = ":S";
+ private static final String DESC = "Show variables, classes or imports";
+ private static final String USAGE = "Usage: :show [variables|imports|all]";
+ private static final String HELP = USAGE + "\n"
+ + " variables - list all bound variables (default)\n"
+ + " imports - list active import statements\n"
+ + " all - list both variables and imports";
+
+ private final ImportCommand importCommand;
+
+
+ public ShowCommand(GroovyEngine engine, Terminal terminal, ImportCommand importCommand) {
+ super(engine, terminal, NAME, SHORTCUT, DESC, USAGE, HELP);
+ this.importCommand = importCommand;
+ }
+
+ @Override
+ public Object execute(List args) {
+ String what = (args == null || args.isEmpty()) ? "variables" : args.get(0).toLowerCase(Locale.ROOT);
+
+ switch (what) {
+ case "variables":
+ case "vars":
+ showVariables();
+ break;
+ case "imports":
+ showImports();
+ break;
+ case "all":
+ showVariables();
+ terminal.writer().println();
+ showImports();
+ break;
+ default:
+ terminal.writer().println(USAGE);
+ break;
+ }
+
+ terminal.writer().flush();
+ return null;
+ }
+
+ private void showVariables() {
+ Map variables = engine.getVariables();
+ if (variables == null || variables.isEmpty()) {
+ terminal.writer().println("No variables defined.");
+ return;
+ }
+
+ terminal.writer().println("Variables:");
+ variables.forEach((name, value) -> {
+ String type = (value != null) ? value.getClass().getSimpleName() : "null";
+ String display = (value != null) ? value : "null";
+ terminal.writer().printf(Locale.ROOT, " %-25s (%s) = %s%n", name, type, display);
+ });
+ }
+
+ private void showImports() {
+ if (importCommand == null) {
+ terminal.writer().println("Import tracking not available.");
+ return;
+ }
+ java.util.Set imports = importCommand.getActiveImports();
+ if (imports.isEmpty()) {
+ terminal.writer().println("No imports registered.");
+ } else {
+ terminal.writer().println("Imports:");
+ imports.forEach(i -> terminal.writer().println(" import " + i));
+ }
+ }
+}
\ No newline at end of file
From 63eeb63ed94c0ee8d18ac240147a800d5309d4cb Mon Sep 17 00:00:00 2001
From: bonampak <14160522+bonampak@users.noreply.github.com>
Date: Mon, 30 Mar 2026 18:02:46 +0200
Subject: [PATCH 22/40] KNOX-3278: purge command added.
---
.../org/apache/knox/gateway/shell/Shell.java | 3 +
.../gateway/shell/commands/ImportCommand.java | 12 ++
.../gateway/shell/commands/PurgeCommand.java | 179 ++++++++++++++++++
3 files changed, 194 insertions(+)
create mode 100644 gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/PurgeCommand.java
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java
index 39a209b01e..639b04a33e 100644
--- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java
@@ -25,6 +25,7 @@
import org.apache.knox.gateway.shell.commands.DataSourceCommand;
import org.apache.knox.gateway.shell.commands.ImportCommand;
import org.apache.knox.gateway.shell.commands.LoadCommand;
+import org.apache.knox.gateway.shell.commands.PurgeCommand;
import org.apache.knox.gateway.shell.commands.SelectCommand;
import org.apache.knox.gateway.shell.commands.ShowCommand;
import org.apache.knox.gateway.shell.commands.WebHDFSCommand;
@@ -123,6 +124,7 @@ private static void startInteractiveShell() throws Exception {
WebHDFSCommand hdfsCmd = new WebHDFSCommand(engine, terminal);
ShowCommand showCmd = new ShowCommand(engine, terminal, importCmd);
LoadCommand loadCmd = new LoadCommand(engine, terminal);
+ PurgeCommand purgeCommand = new PurgeCommand(engine, terminal, importCmd);
registerCommand(registry, importCmd);
registerCommand(registry, selectCmd);
@@ -131,6 +133,7 @@ private static void startInteractiveShell() throws Exception {
registerCommand(registry, hdfsCmd);
registerCommand(registry, showCmd);
registerCommand(registry, loadCmd);
+ registerCommand(registry, purgeCommand);
Map commandMethods = new HashMap<>();
Map commandAliases = new HashMap<>();
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ImportCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ImportCommand.java
index 80189933ee..62faef05d7 100644
--- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ImportCommand.java
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ImportCommand.java
@@ -66,6 +66,18 @@ public Set getActiveImports() {
return Collections.unmodifiableSet(activeImports);
}
+ /**
+ * Removes an import from the tracked set.
+ * Note: this does not truly "unimport" from the Groovy classloader,
+ * but it removes it from the tracked listing and from future :show output.
+ *
+ * @param fqcn the fully-qualified class or package.* to remove
+ * @return true if the import was present and removed
+ */
+ public boolean removeImport(String fqcn) {
+ return activeImports.remove(fqcn);
+ }
+
@Override
public Object execute(List args) {
if (args == null || args.isEmpty()) {
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/PurgeCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/PurgeCommand.java
new file mode 100644
index 0000000000..9787a2b941
--- /dev/null
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/PurgeCommand.java
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.knox.gateway.shell.commands;
+
+import org.apache.groovy.groovysh.jline.GroovyEngine;
+import org.jline.reader.Candidate;
+import org.jline.reader.Completer;
+import org.jline.reader.impl.completer.ArgumentCompleter;
+import org.jline.reader.impl.completer.NullCompleter;
+import org.jline.reader.impl.completer.StringsCompleter;
+import org.jline.terminal.Terminal;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Clears variables, imports or both from the current shell session.
+ * Internal Knox variables (prefixed with __knox) are preserved by default.
+ *
+ * Usage:
+ *
+ * - :purge - clears user variables (preserves internal Knox state)
+ * - :purge variables - same as above
+ * - :purge imports - clears user-added imports (preserves built-in Knox imports)
+ * - :purge all - clears both variables and user imports
+ *
+ *
+ */
+public class PurgeCommand extends AbstractKnoxShellCommand {
+
+ private static final String NAME = ":purge";
+ private static final String SHORTCUT = ":p";
+ private static final String DESC = "Purge variables, classes, imports or preferences";
+ private static final String USAGE = "Usage: :purge [variables|imports|all]";
+ private static final String HELP = USAGE + "\n"
+ + " variables - purge user variables, keep internal Knox state (default)\n"
+ + " imports - purge user-added imports, keep built-in Knox imports\n"
+ + " all - purge both variables and user imports";
+
+ /** Prefix used by Knox internal bindings (__knoxdatasource, __knoxsession, etc.) */
+ private static final String KNOX_INTERNAL_PREFIX = "__knox";
+
+ private final ImportCommand importCommand;
+ private final Set builtInImports;
+
+ /**
+ * @param engine the GroovyEngine
+ * @param terminal the JLine terminal
+ * @param importCommand the ImportCommand instance (for clearing/resetting imports)
+ */
+ public PurgeCommand(GroovyEngine engine, Terminal terminal, ImportCommand importCommand) {
+ super(engine, terminal, NAME, SHORTCUT, DESC, USAGE, HELP);
+ this.importCommand = importCommand;
+ // Snapshot the built-in imports at construction time so we know which ones to preserve
+ this.builtInImports = Set.copyOf(importCommand.getActiveImports());
+ }
+
+ @Override
+ public Object execute(List args) {
+ String what = (args == null || args.isEmpty()) ? "variables" : args.get(0).toLowerCase(Locale.ROOT);
+
+ switch (what) {
+ case "variables":
+ case "vars":
+ int varCount = clearVariables();
+ terminal.writer().println("Purged " + varCount + " variable(s). Internal Knox state preserved.");
+ break;
+ case "imports":
+ int importCount = clearUserImports();
+ terminal.writer().println("Purged " + importCount + " user import(s). Built-in Knox imports preserved.");
+ break;
+ case "all":
+ int vc = clearVariables();
+ int ic = clearUserImports();
+ terminal.writer().println("Purged " + vc + " variable(s) and " + ic + " user import(s).");
+ break;
+ default:
+ terminal.writer().println(USAGE);
+ break;
+ }
+
+ terminal.writer().flush();
+ return null;
+ }
+
+ private int clearVariables() {
+ Map variables = engine.getVariables();
+ if (variables == null || variables.isEmpty()) {
+ return 0;
+ }
+
+ int count = 0;
+ Iterator> it = variables.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry entry = it.next();
+ // Preserve internal Knox bindings
+ if (!entry.getKey().startsWith(KNOX_INTERNAL_PREFIX)) {
+ it.remove();
+ count++;
+ }
+ }
+ return count;
+ }
+
+ private int clearUserImports() {
+ if (importCommand == null) {
+ return 0;
+ }
+
+ Set current = importCommand.getActiveImports();
+ // Collect user-added imports (those not in the built-in snapshot)
+ List toRemove = current.stream()
+ .filter(imp -> !builtInImports.contains(imp))
+ .toList();
+
+ // Remove from ImportCommand's tracked set
+ toRemove.forEach(importCommand::removeImport);
+
+ return toRemove.size();
+ }
+
+ @Override
+ public List getCompleters() {
+ // Index 0: command name placeholder (Shell.java handles routing)
+ Completer commandPlaceholder = (reader, parsedLine, candidates) -> {};
+
+ // Index 1: subcommands
+ Completer subCommandCompleter = new StringsCompleter("variables", "imports", "all");
+
+ // Index 2: dynamic target names (variable names for "variables", import names for "imports")
+ Completer targetCompleter = (reader, parsedLine, candidates) -> {
+ List words = parsedLine.words();
+ if (words.size() > 1) {
+ String subCommand = words.get(1).toLowerCase(Locale.ROOT);
+ if ("variables".equals(subCommand) || "vars".equals(subCommand)) {
+ // Suggest purgeable variable names (exclude internal __knox* ones)
+ Map variables = engine.getVariables();
+ if (variables != null) {
+ variables.keySet().stream()
+ .filter(name -> !name.startsWith(KNOX_INTERNAL_PREFIX))
+ .forEach(name -> candidates.add(new Candidate(name)));
+ }
+ } else if ("imports".equals(subCommand)) {
+ // Suggest user-added imports (exclude built-in Knox imports)
+ importCommand.getActiveImports().stream()
+ .filter(imp -> !builtInImports.contains(imp))
+ .forEach(imp -> candidates.add(new Candidate(imp)));
+ }
+ }
+ };
+
+ ArgumentCompleter argCompleter = new ArgumentCompleter(
+ commandPlaceholder,
+ subCommandCompleter,
+ targetCompleter,
+ NullCompleter.INSTANCE);
+
+ return Collections.singletonList(argCompleter);
+ }
+}
\ No newline at end of file
From 0ac946d0ef061d67d73ac33ee1d7cdd12ff0038b Mon Sep 17 00:00:00 2001
From: bonampak <14160522+bonampak@users.noreply.github.com>
Date: Tue, 31 Mar 2026 19:35:47 +0200
Subject: [PATCH 23/40] KNOX-3278: Correcting load command and alias.
---
.../org/apache/knox/gateway/shell/Shell.java | 24 ++++++++++++-------
.../gateway/shell/commands/LoadCommand.java | 23 ++++++++++++++++--
2 files changed, 36 insertions(+), 11 deletions(-)
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java
index 639b04a33e..5ebfa8c8b9 100644
--- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java
@@ -140,15 +140,17 @@ private static void startInteractiveShell() throws Exception {
registry.forEach((rawName, cmd) -> {
+ if (rawName.equals(cmd.getShortcut())) {
+ return;
+ }
+ String name = rawName.startsWith(":") ? rawName : ":" + rawName;
- // 1. THE FIX: Force the JLine registry to know the commands start with a colon
- String name = rawName.startsWith(":") ? rawName : ":" + rawName;
-
- String rawShortcut = cmd.getShortcut();
- if (rawShortcut != null && !rawShortcut.isEmpty()) {
- String shortcut = rawShortcut.startsWith(":") ? rawShortcut : ":" + rawShortcut;
- commandAliases.put(shortcut, name);
- }
+ String rawShortcut = cmd.getShortcut();
+ String shortcut = null;
+ if (rawShortcut != null && !rawShortcut.isEmpty()) {
+ shortcut = rawShortcut.equals(".") || rawShortcut.startsWith(":") ? rawShortcut : ":" + rawShortcut;
+ commandAliases.put(shortcut, name);
+ }
// 2. Put them in the map with the colon-prefixed names
commandMethods.put(name, new CommandMethods(
@@ -176,6 +178,9 @@ private static void startInteractiveShell() throws Exception {
});
DefaultParser parser = new DefaultParser();
+ // Override default regex to allow '.' as a valid command string
+ // Original: "[:]?[a-zA-Z]+[a-zA-Z0-9_-]*"
+ parser.setRegexCommand("(?:\\.|[:]?[a-zA-Z]+[a-zA-Z0-9_-]*)");
Path workDir = Paths.get(System.getProperty("user.dir"));
SimpleCommandRegistry knoxRegistry = new SimpleCommandRegistry(commandMethods, commandAliases);
SystemRegistry systemRegistry = new SystemRegistryImpl(parser, terminal, () -> workDir, null);
@@ -192,13 +197,14 @@ private static void startInteractiveShell() throws Exception {
// 5. Build the LineReader
LineReader reader = LineReaderBuilder.builder()
+ .parser(parser)
.terminal(terminal)
.completer(combinedCompleter)
.variable(LineReader.HISTORY_FILE, Paths.get(System.getProperty("user.home"), ".knoxshell_history"))
.build();
terminal.writer().println("Apache Knox Shell");
- terminal.writer().println("Type ':help' for help, ':exit' or ':quit' (':x' or ':q') to quit.");
+ terminal.writer().println("Type ':help' (':h' or '?') for help, ':exit' or ':quit' (':x' or ':q') to quit.");
terminal.writer().flush();
// 6. Setup Shutdown Hook (Calling closeConnections directly on our object instances)
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoadCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoadCommand.java
index 982ed51346..43652f4ea8 100644
--- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoadCommand.java
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoadCommand.java
@@ -18,6 +18,9 @@
package org.apache.knox.gateway.shell.commands;
import org.apache.groovy.groovysh.jline.GroovyEngine;
+import org.jline.builtins.Completers;
+import org.jline.reader.Completer;
+import org.jline.reader.impl.completer.NullCompleter;
import org.jline.terminal.Terminal;
import java.io.BufferedReader;
@@ -29,6 +32,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
@@ -127,9 +131,10 @@ private String readScript(String location) throws IOException {
}
private boolean isUrl(String location) {
- return location.startsWith("http://")
+ return location!=null &&
+ (location.startsWith("http://")
|| location.startsWith("https://")
- || location.startsWith("file://");
+ || location.startsWith("file://"));
}
private String readFromUrl(String urlStr) throws IOException {
@@ -145,4 +150,18 @@ private String readFromUrl(String urlStr) throws IOException {
return reader.lines().collect(Collectors.joining("\n"));
}
}
+
+ @Override
+ public List getCompleters() {
+ Completers.FileNameCompleter fileNameCompleter = new Completers.FileNameCompleter();
+ Completer fileCompleter = (reader, parsedLine, candidates) -> {
+ String word = parsedLine.word();
+ if (isUrl(word)) {
+ return;
+ }
+ fileNameCompleter.complete(reader, parsedLine, candidates);
+ };
+ return Arrays.asList(fileCompleter, NullCompleter.INSTANCE);
+ }
+
}
From ced80966ae5522e87126b64bd27380be9910c790 Mon Sep 17 00:00:00 2001
From: bonampak <14160522+bonampak@users.noreply.github.com>
Date: Tue, 31 Mar 2026 19:42:13 +0200
Subject: [PATCH 24/40] KNOX-3278: Adding completer for ShowCommand.
---
.../knox/gateway/shell/commands/ShowCommand.java | 15 +++++++++++++--
1 file changed, 13 insertions(+), 2 deletions(-)
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ShowCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ShowCommand.java
index d673c05e8e..0952e4478f 100644
--- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ShowCommand.java
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ShowCommand.java
@@ -18,8 +18,12 @@
package org.apache.knox.gateway.shell.commands;
import org.apache.groovy.groovysh.jline.GroovyEngine;
+import org.jline.reader.Completer;
+import org.jline.reader.impl.completer.NullCompleter;
+import org.jline.reader.impl.completer.StringsCompleter;
import org.jline.terminal.Terminal;
+import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -37,8 +41,8 @@ public class ShowCommand extends AbstractKnoxShellCommand {
private static final String NAME = ":show";
private static final String SHORTCUT = ":S";
- private static final String DESC = "Show variables, classes or imports";
- private static final String USAGE = "Usage: :show [variables|imports|all]";
+ private static final String DESC = "Show variables, imports or both";
+ private static final String USAGE = "Usage: :show [variables|vars|imports|all]";
private static final String HELP = USAGE + "\n"
+ " variables - list all bound variables (default)\n"
+ " imports - list active import statements\n"
@@ -106,4 +110,11 @@ private void showImports() {
imports.forEach(i -> terminal.writer().println(" import " + i));
}
}
+
+ @Override
+ public List getCompleters() {
+ Completer subCommandCompleter = new StringsCompleter("variables", "vars", "imports", "all");
+ return Arrays.asList(subCommandCompleter, NullCompleter.INSTANCE);
+ }
+
}
\ No newline at end of file
From 783aee2e362622561a06e468793a86a751887a69 Mon Sep 17 00:00:00 2001
From: bonampak <14160522+bonampak@users.noreply.github.com>
Date: Wed, 1 Apr 2026 11:45:11 +0200
Subject: [PATCH 25/40] KNOX-3278: Correct checkstyle error on shortcut
handling
---
.../src/main/java/org/apache/knox/gateway/shell/Shell.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java
index 5ebfa8c8b9..7f0979a258 100644
--- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java
@@ -148,7 +148,7 @@ private static void startInteractiveShell() throws Exception {
String rawShortcut = cmd.getShortcut();
String shortcut = null;
if (rawShortcut != null && !rawShortcut.isEmpty()) {
- shortcut = rawShortcut.equals(".") || rawShortcut.startsWith(":") ? rawShortcut : ":" + rawShortcut;
+ shortcut = (".".equals(rawShortcut) || ":".equals(rawShortcut)) ? rawShortcut : ":" + rawShortcut;
commandAliases.put(shortcut, name);
}
From 531ccb0a5aeede6e984cf38ff281a96054cb25ff Mon Sep 17 00:00:00 2001
From: bonampak <14160522+bonampak@users.noreply.github.com>
Date: Wed, 8 Apr 2026 20:40:16 +0200
Subject: [PATCH 26/40] KNOX-3278: Renaming SimpleCommandRegistry to
KnoxShellCommandRegistry and cleaning up command handling. Overriding name()
so that tab completion after semicolon does not show the name of the command
registry.
---
...stry.java => KnoxShellCommandRegistry.java} | 9 +++++++--
.../org/apache/knox/gateway/shell/Shell.java | 18 +++++++-----------
2 files changed, 14 insertions(+), 13 deletions(-)
rename gateway-shell/src/main/java/org/apache/knox/gateway/shell/{SimpleCommandRegistry.java => KnoxShellCommandRegistry.java} (93%)
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/SimpleCommandRegistry.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/KnoxShellCommandRegistry.java
similarity index 93%
rename from gateway-shell/src/main/java/org/apache/knox/gateway/shell/SimpleCommandRegistry.java
rename to gateway-shell/src/main/java/org/apache/knox/gateway/shell/KnoxShellCommandRegistry.java
index 1e67a47f6a..a401b67172 100644
--- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/SimpleCommandRegistry.java
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/KnoxShellCommandRegistry.java
@@ -29,16 +29,21 @@
import java.util.Map;
import java.util.Set;
-public class SimpleCommandRegistry implements CommandRegistry {
+public class KnoxShellCommandRegistry implements CommandRegistry {
private final Map commands;
private final Map aliases;
- public SimpleCommandRegistry(Map commands, Map aliases) {
+ public KnoxShellCommandRegistry(Map commands, Map aliases) {
this.commands = commands;
this.aliases = aliases != null ? aliases : Collections.emptyMap();
}
+ @Override
+ public String name() {
+ return "";
+ }
+
@Override
public boolean hasCommand(String command) {
return commands.containsKey(command) || aliases.containsKey(command);
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java
index 7f0979a258..1b9488f9f4 100644
--- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java
@@ -139,16 +139,12 @@ private static void startInteractiveShell() throws Exception {
Map commandAliases = new HashMap<>();
- registry.forEach((rawName, cmd) -> {
- if (rawName.equals(cmd.getShortcut())) {
+ registry.forEach((name, cmd) -> {
+ if (name.equals(cmd.getShortcut())) {
return;
}
- String name = rawName.startsWith(":") ? rawName : ":" + rawName;
-
- String rawShortcut = cmd.getShortcut();
- String shortcut = null;
- if (rawShortcut != null && !rawShortcut.isEmpty()) {
- shortcut = (".".equals(rawShortcut) || ":".equals(rawShortcut)) ? rawShortcut : ":" + rawShortcut;
+ String shortcut = cmd.getShortcut();
+ if (shortcut != null && !shortcut.isEmpty()) {
commandAliases.put(shortcut, name);
}
@@ -182,9 +178,9 @@ private static void startInteractiveShell() throws Exception {
// Original: "[:]?[a-zA-Z]+[a-zA-Z0-9_-]*"
parser.setRegexCommand("(?:\\.|[:]?[a-zA-Z]+[a-zA-Z0-9_-]*)");
Path workDir = Paths.get(System.getProperty("user.dir"));
- SimpleCommandRegistry knoxRegistry = new SimpleCommandRegistry(commandMethods, commandAliases);
+ KnoxShellCommandRegistry knoxShellCommandRegistry = new KnoxShellCommandRegistry(commandMethods, commandAliases);
SystemRegistry systemRegistry = new SystemRegistryImpl(parser, terminal, () -> workDir, null);
- systemRegistry.setCommandRegistries(knoxRegistry);
+ systemRegistry.setCommandRegistries(knoxShellCommandRegistry);
SystemRegistry.add(systemRegistry);
// 4. Setup Tab Completers
@@ -281,7 +277,7 @@ private static void startInteractiveShell() throws Exception {
Object res = cmd.execute(cmdArgs);
if (res != null) {
- terminal.writer().println(res.toString());
+ terminal.writer().println(res);
}
} else {
// Fallback to evaluating standard Groovy script logic
From b06371bc595914991a9e0fcd39a11ae2d986b499 Mon Sep 17 00:00:00 2001
From: bonampak <14160522+bonampak@users.noreply.github.com>
Date: Wed, 15 Apr 2026 11:57:38 +0200
Subject: [PATCH 27/40] KNOX-3278: No need to exclude Log4j2Plugins.dat from
the shaded knoxhsell jar.
---
gateway-shell-release/pom.xml | 1 -
1 file changed, 1 deletion(-)
diff --git a/gateway-shell-release/pom.xml b/gateway-shell-release/pom.xml
index 3c44f90a1d..31d47caef3 100644
--- a/gateway-shell-release/pom.xml
+++ b/gateway-shell-release/pom.xml
@@ -58,7 +58,6 @@
schema/**
**/*.ldif
- META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat
From b08b20e4811e37089f1314d597558b79741b3788 Mon Sep 17 00:00:00 2001
From: bonampak <14160522+bonampak@users.noreply.github.com>
Date: Wed, 15 Apr 2026 11:58:12 +0200
Subject: [PATCH 28/40] KNOX-3278: correcting knoxshell.sh to use -jar instead
of main class (to use Launcher taken from knoxshell.jar manifest main class)
---
gateway-shell-release/home/bin/knoxshell.sh | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/gateway-shell-release/home/bin/knoxshell.sh b/gateway-shell-release/home/bin/knoxshell.sh
index 14cea81e79..877441502c 100755
--- a/gateway-shell-release/home/bin/knoxshell.sh
+++ b/gateway-shell-release/home/bin/knoxshell.sh
@@ -81,7 +81,7 @@ function main {
checkJava
buildAppJavaOpts
- $JAVA "${APP_JAVA_OPTS[@]}" -Dlog4j.configurationFile=conf/knoxshell-log4j2.xml -javaagent:"$APP_BIN_DIR"/../lib/aspectjweaver.jar -cp "$APP_JAR":lib/* org.apache.knox.gateway.shell.Shell "$@" || exit 1
+ $JAVA "${APP_JAVA_OPTS[@]}" -Dlog4j.configurationFile=conf/knoxshell-log4j2.xml -javaagent:"$APP_BIN_DIR"/../lib/aspectjweaver.jar -cp "$APP_JAR":lib/* -jar "$APP_JAR" "$@" || exit 1
return 0
}
From ca30d3684cb7c6ad86952d287a88779afc7b8bb2 Mon Sep 17 00:00:00 2001
From: bonampak <14160522+bonampak@users.noreply.github.com>
Date: Wed, 15 Apr 2026 16:34:35 +0200
Subject: [PATCH 29/40] KNOX-3278: excluding Log4j2Plugins.dat from the shaded
knoxhsell jar and making knoxshell-log4j2.xml consistent with other log4j2
configurations.
---
gateway-shell-release/home/conf/knoxshell-log4j2.xml | 2 +-
gateway-shell-release/pom.xml | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/gateway-shell-release/home/conf/knoxshell-log4j2.xml b/gateway-shell-release/home/conf/knoxshell-log4j2.xml
index b8997b0302..275094a344 100644
--- a/gateway-shell-release/home/conf/knoxshell-log4j2.xml
+++ b/gateway-shell-release/home/conf/knoxshell-log4j2.xml
@@ -17,7 +17,7 @@
-->
- logs
+ ${sys:launcher.dir}/../logs
${sys:launcher.name}.log
diff --git a/gateway-shell-release/pom.xml b/gateway-shell-release/pom.xml
index 31d47caef3..3c44f90a1d 100644
--- a/gateway-shell-release/pom.xml
+++ b/gateway-shell-release/pom.xml
@@ -58,6 +58,7 @@
schema/**
**/*.ldif
+ META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat
From 5556484cff5950e8a9433e62c55943da7432666e Mon Sep 17 00:00:00 2001
From: bonampak <14160522+bonampak@users.noreply.github.com>
Date: Wed, 15 Apr 2026 17:40:04 +0200
Subject: [PATCH 30/40] KNOX-3278: get rid of useless warning javax.* types are
not being woven
---
gateway-shell/src/main/resources/META-INF/aop.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/gateway-shell/src/main/resources/META-INF/aop.xml b/gateway-shell/src/main/resources/META-INF/aop.xml
index 0070403377..7f4fa2514a 100644
--- a/gateway-shell/src/main/resources/META-INF/aop.xml
+++ b/gateway-shell/src/main/resources/META-INF/aop.xml
@@ -20,7 +20,7 @@
-
+
From ddc891a30b6816b4d49bc58661f64f94f97c9050 Mon Sep 17 00:00:00 2001
From: bonampak <14160522+bonampak@users.noreply.github.com>
Date: Wed, 15 Apr 2026 18:16:49 +0200
Subject: [PATCH 31/40] KNOX-3278: fix log4j2 WARNING:
sun.reflect.Reflection.getCallerClass is not supported. This will impact
performance.
---
gateway-shell-release/pom.xml | 3 +++
1 file changed, 3 insertions(+)
diff --git a/gateway-shell-release/pom.xml b/gateway-shell-release/pom.xml
index 3c44f90a1d..0724c460a0 100644
--- a/gateway-shell-release/pom.xml
+++ b/gateway-shell-release/pom.xml
@@ -49,6 +49,9 @@
org.apache.knox.gateway.launcher.Launcher
+
+ true
+
From 294fc5be4a797d48e15c1acd7ad41fade23b9e51 Mon Sep 17 00:00:00 2001
From: bonampak <14160522+bonampak@users.noreply.github.com>
Date: Thu, 16 Apr 2026 14:21:54 +0200
Subject: [PATCH 32/40] KNOX-3278: Fix purge command, simplify import and show
commands.
---
.../org/apache/knox/gateway/shell/Shell.java | 12 +--
.../gateway/shell/commands/ImportCommand.java | 43 ++------
.../gateway/shell/commands/PurgeCommand.java | 97 +++++--------------
.../gateway/shell/commands/ShowCommand.java | 21 ++--
4 files changed, 44 insertions(+), 129 deletions(-)
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java
index 1b9488f9f4..029ed355e2 100644
--- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java
@@ -110,21 +110,21 @@ private static void startInteractiveShell() throws Exception {
GroovyEngine engine = new GroovyEngine();
// 2. Pre-load Knox imports
- ImportCommand importCmd = new ImportCommand(engine, terminal);
for (String name : IMPORTS) {
engine.execute("import " + name);
- importCmd.registerBuiltIn(name);
}
// 3. Instantiate and Map Custom Commands
Map registry = new HashMap<>();
- SelectCommand selectCmd = new SelectCommand(engine, terminal);
- DataSourceCommand dsCmd = new DataSourceCommand(engine, terminal);
CSVCommand csvCmd = new CSVCommand(engine, terminal);
+ DataSourceCommand dsCmd = new DataSourceCommand(engine, terminal);
+ SelectCommand selectCmd = new SelectCommand(engine, terminal);
WebHDFSCommand hdfsCmd = new WebHDFSCommand(engine, terminal);
- ShowCommand showCmd = new ShowCommand(engine, terminal, importCmd);
+
+ ImportCommand importCmd = new ImportCommand(engine, terminal);
LoadCommand loadCmd = new LoadCommand(engine, terminal);
- PurgeCommand purgeCommand = new PurgeCommand(engine, terminal, importCmd);
+ PurgeCommand purgeCommand = new PurgeCommand(engine, terminal);
+ ShowCommand showCmd = new ShowCommand(engine, terminal);
registerCommand(registry, importCmd);
registerCommand(registry, selectCmd);
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ImportCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ImportCommand.java
index 62faef05d7..e607788195 100644
--- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ImportCommand.java
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ImportCommand.java
@@ -20,10 +20,8 @@
import org.apache.groovy.groovysh.jline.GroovyEngine;
import org.jline.terminal.Terminal;
-import java.util.Collections;
-import java.util.LinkedHashSet;
import java.util.List;
-import java.util.Set;
+import java.util.Map;
/**
* Manages Groovy imports in the current shell session.
@@ -44,50 +42,22 @@ public class ImportCommand extends AbstractKnoxShellCommand {
+ " :import .* - wildcard import";
private static final String HELP = USAGE;
- private final Set activeImports = new LinkedHashSet<>();
-
public ImportCommand(GroovyEngine engine, Terminal terminal) {
super(engine, terminal, NAME, SHORTCUT, DESC, USAGE, HELP);
}
- /**
- * Registers a built-in import so it appears in the :import listing.
- * Called from Shell during startup for each pre-loaded Knox import.
- */
- public void registerBuiltIn(String fqcn) {
- activeImports.add(fqcn);
- }
-
- /**
- * Returns an unmodifiable view of the currently active imports.
- * Used by ShowCommand to display imports via :show imports.
- */
- public Set getActiveImports() {
- return Collections.unmodifiableSet(activeImports);
- }
-
- /**
- * Removes an import from the tracked set.
- * Note: this does not truly "unimport" from the Groovy classloader,
- * but it removes it from the tracked listing and from future :show output.
- *
- * @param fqcn the fully-qualified class or package.* to remove
- * @return true if the import was present and removed
- */
- public boolean removeImport(String fqcn) {
- return activeImports.remove(fqcn);
- }
-
@Override
public Object execute(List args) {
if (args == null || args.isEmpty()) {
// List mode
- if (activeImports.isEmpty()) {
+ Map imports = engine.getImports();
+ if (imports.isEmpty()) {
terminal.writer().println("No imports registered.");
} else {
terminal.writer().println("Active imports:");
- activeImports.forEach(i -> terminal.writer().println(" import " + i));
- }
+ imports.keySet().stream()
+ .sorted()
+ .forEach(key -> terminal.writer().println(" import " + key)); }
terminal.writer().flush();
return null;
}
@@ -110,7 +80,6 @@ public Object execute(List args) {
try {
engine.execute("import " + target);
- activeImports.add(target);
terminal.writer().println("==> import " + target);
} catch (Exception e) {
terminal.writer().println("Failed to import '" + target + "': " + e.getMessage());
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/PurgeCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/PurgeCommand.java
index 9787a2b941..92122bc475 100644
--- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/PurgeCommand.java
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/PurgeCommand.java
@@ -18,19 +18,15 @@
package org.apache.knox.gateway.shell.commands;
import org.apache.groovy.groovysh.jline.GroovyEngine;
-import org.jline.reader.Candidate;
import org.jline.reader.Completer;
-import org.jline.reader.impl.completer.ArgumentCompleter;
import org.jline.reader.impl.completer.NullCompleter;
import org.jline.reader.impl.completer.StringsCompleter;
import org.jline.terminal.Terminal;
-import java.util.Collections;
-import java.util.Iterator;
+import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;
-import java.util.Set;
/**
* Clears variables, imports or both from the current shell session.
@@ -52,26 +48,19 @@ public class PurgeCommand extends AbstractKnoxShellCommand {
private static final String DESC = "Purge variables, classes, imports or preferences";
private static final String USAGE = "Usage: :purge [variables|imports|all]";
private static final String HELP = USAGE + "\n"
- + " variables - purge user variables, keep internal Knox state (default)\n"
+ + " variables - purge user variables, keep internal Knox state\n"
+ " imports - purge user-added imports, keep built-in Knox imports\n"
+ " all - purge both variables and user imports";
/** Prefix used by Knox internal bindings (__knoxdatasource, __knoxsession, etc.) */
private static final String KNOX_INTERNAL_PREFIX = "__knox";
- private final ImportCommand importCommand;
- private final Set builtInImports;
-
/**
* @param engine the GroovyEngine
* @param terminal the JLine terminal
- * @param importCommand the ImportCommand instance (for clearing/resetting imports)
*/
- public PurgeCommand(GroovyEngine engine, Terminal terminal, ImportCommand importCommand) {
+ public PurgeCommand(GroovyEngine engine, Terminal terminal) {
super(engine, terminal, NAME, SHORTCUT, DESC, USAGE, HELP);
- this.importCommand = importCommand;
- // Snapshot the built-in imports at construction time so we know which ones to preserve
- this.builtInImports = Set.copyOf(importCommand.getActiveImports());
}
@Override
@@ -80,18 +69,17 @@ public Object execute(List args) {
switch (what) {
case "variables":
- case "vars":
int varCount = clearVariables();
terminal.writer().println("Purged " + varCount + " variable(s). Internal Knox state preserved.");
break;
case "imports":
- int importCount = clearUserImports();
- terminal.writer().println("Purged " + importCount + " user import(s). Built-in Knox imports preserved.");
+ int importCount = clearImports();
+ terminal.writer().println("Purged " + importCount + " import(s).");
break;
case "all":
int vc = clearVariables();
- int ic = clearUserImports();
- terminal.writer().println("Purged " + vc + " variable(s) and " + ic + " user import(s).");
+ int ic = clearImports();
+ terminal.writer().println("Purged " + vc + " variable(s) and " + ic + " import(s).");
break;
default:
terminal.writer().println(USAGE);
@@ -103,77 +91,44 @@ public Object execute(List args) {
}
private int clearVariables() {
- Map variables = engine.getVariables();
+ java.util.Map variables = engine.find();
if (variables == null || variables.isEmpty()) {
return 0;
}
int count = 0;
- Iterator> it = variables.entrySet().iterator();
- while (it.hasNext()) {
- Map.Entry entry = it.next();
+ List keysToDelete = new java.util.ArrayList<>();
+ for (String variableName : variables.keySet()) {
// Preserve internal Knox bindings
- if (!entry.getKey().startsWith(KNOX_INTERNAL_PREFIX)) {
- it.remove();
+ if (variableName != null && !variableName.startsWith(KNOX_INTERNAL_PREFIX)) {
+ keysToDelete.add(variableName);
count++;
}
}
+ if (!keysToDelete.isEmpty()) {
+ engine.del(keysToDelete.toArray(new String[0]));
+ }
return count;
}
- private int clearUserImports() {
- if (importCommand == null) {
+ private int clearImports() {
+ Map imports = engine.getImports();
+
+ if (imports == null || imports.isEmpty()) {
return 0;
}
- Set current = importCommand.getActiveImports();
- // Collect user-added imports (those not in the built-in snapshot)
- List toRemove = current.stream()
- .filter(imp -> !builtInImports.contains(imp))
- .toList();
-
- // Remove from ImportCommand's tracked set
- toRemove.forEach(importCommand::removeImport);
-
- return toRemove.size();
+ int count = 0;
+ for (String importName : imports.keySet()) {
+ engine.removeImport(importName);
+ count++;
+ }
+ return count;
}
@Override
public List getCompleters() {
- // Index 0: command name placeholder (Shell.java handles routing)
- Completer commandPlaceholder = (reader, parsedLine, candidates) -> {};
-
- // Index 1: subcommands
Completer subCommandCompleter = new StringsCompleter("variables", "imports", "all");
-
- // Index 2: dynamic target names (variable names for "variables", import names for "imports")
- Completer targetCompleter = (reader, parsedLine, candidates) -> {
- List words = parsedLine.words();
- if (words.size() > 1) {
- String subCommand = words.get(1).toLowerCase(Locale.ROOT);
- if ("variables".equals(subCommand) || "vars".equals(subCommand)) {
- // Suggest purgeable variable names (exclude internal __knox* ones)
- Map variables = engine.getVariables();
- if (variables != null) {
- variables.keySet().stream()
- .filter(name -> !name.startsWith(KNOX_INTERNAL_PREFIX))
- .forEach(name -> candidates.add(new Candidate(name)));
- }
- } else if ("imports".equals(subCommand)) {
- // Suggest user-added imports (exclude built-in Knox imports)
- importCommand.getActiveImports().stream()
- .filter(imp -> !builtInImports.contains(imp))
- .forEach(imp -> candidates.add(new Candidate(imp)));
- }
- }
- };
-
- ArgumentCompleter argCompleter = new ArgumentCompleter(
- commandPlaceholder,
- subCommandCompleter,
- targetCompleter,
- NullCompleter.INSTANCE);
-
- return Collections.singletonList(argCompleter);
+ return Arrays.asList(subCommandCompleter, NullCompleter.INSTANCE);
}
}
\ No newline at end of file
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ShowCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ShowCommand.java
index 0952e4478f..aba386fba8 100644
--- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ShowCommand.java
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ShowCommand.java
@@ -42,18 +42,14 @@ public class ShowCommand extends AbstractKnoxShellCommand {
private static final String NAME = ":show";
private static final String SHORTCUT = ":S";
private static final String DESC = "Show variables, imports or both";
- private static final String USAGE = "Usage: :show [variables|vars|imports|all]";
+ private static final String USAGE = "Usage: :show [variables|imports|all]";
private static final String HELP = USAGE + "\n"
+ " variables - list all bound variables (default)\n"
+ " imports - list active import statements\n"
+ " all - list both variables and imports";
- private final ImportCommand importCommand;
-
-
- public ShowCommand(GroovyEngine engine, Terminal terminal, ImportCommand importCommand) {
+ public ShowCommand(GroovyEngine engine, Terminal terminal) {
super(engine, terminal, NAME, SHORTCUT, DESC, USAGE, HELP);
- this.importCommand = importCommand;
}
@Override
@@ -62,7 +58,6 @@ public Object execute(List args) {
switch (what) {
case "variables":
- case "vars":
showVariables();
break;
case "imports":
@@ -83,7 +78,7 @@ public Object execute(List args) {
}
private void showVariables() {
- Map variables = engine.getVariables();
+ Map variables = engine.find();
if (variables == null || variables.isEmpty()) {
terminal.writer().println("No variables defined.");
return;
@@ -92,17 +87,13 @@ private void showVariables() {
terminal.writer().println("Variables:");
variables.forEach((name, value) -> {
String type = (value != null) ? value.getClass().getSimpleName() : "null";
- String display = (value != null) ? value : "null";
+ String display = (value != null) ? value.toString() : "null";
terminal.writer().printf(Locale.ROOT, " %-25s (%s) = %s%n", name, type, display);
});
}
private void showImports() {
- if (importCommand == null) {
- terminal.writer().println("Import tracking not available.");
- return;
- }
- java.util.Set imports = importCommand.getActiveImports();
+ java.util.Set imports = engine.getImports().keySet();
if (imports.isEmpty()) {
terminal.writer().println("No imports registered.");
} else {
@@ -113,7 +104,7 @@ private void showImports() {
@Override
public List getCompleters() {
- Completer subCommandCompleter = new StringsCompleter("variables", "vars", "imports", "all");
+ Completer subCommandCompleter = new StringsCompleter("variables", "imports", "all");
return Arrays.asList(subCommandCompleter, NullCompleter.INSTANCE);
}
From 206050b98e2ea62b35a1a4d813e7fc0cab977037 Mon Sep 17 00:00:00 2001
From: bonampak <14160522+bonampak@users.noreply.github.com>
Date: Thu, 16 Apr 2026 18:32:01 +0200
Subject: [PATCH 33/40] KNOX-3278: Fix ImportCommand and add support for static
imports.
---
.../gateway/shell/commands/ImportCommand.java | 30 ++++++++++---------
1 file changed, 16 insertions(+), 14 deletions(-)
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ImportCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ImportCommand.java
index e607788195..ecee87b95d 100644
--- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ImportCommand.java
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ImportCommand.java
@@ -22,6 +22,7 @@
import java.util.List;
import java.util.Map;
+import java.util.regex.Pattern;
/**
* Manages Groovy imports in the current shell session.
@@ -39,9 +40,14 @@ public class ImportCommand extends AbstractKnoxShellCommand {
private static final String USAGE = "Usage: :import []\n"
+ " :import - list active imports\n"
+ " :import - add a new import\n"
- + " :import .* - wildcard import";
+ + " :import static - add a static import\n"
+ + " :import .* - wildcard import";
private static final String HELP = USAGE;
+ // Groovysh 4.x validation: chars, digits, underscore, dot, star, optional semicolon
+ private static final Pattern IMPORTED_ITEM_PATTERN = Pattern.compile("^[a-zA-Z0-9_. *]+;?$");
+
+
public ImportCommand(GroovyEngine engine, Terminal terminal) {
super(engine, terminal, NAME, SHORTCUT, DESC, USAGE, HELP);
}
@@ -55,29 +61,25 @@ public Object execute(List args) {
terminal.writer().println("No imports registered.");
} else {
terminal.writer().println("Active imports:");
- imports.keySet().stream()
+ imports.values().stream()
.sorted()
- .forEach(key -> terminal.writer().println(" import " + key)); }
+ .forEach(value -> terminal.writer().println(value)); }
terminal.writer().flush();
return null;
}
- // Join all args in case user typed "import java.util. *" with spaces
- String target = String.join("", args).trim();
+ // Join with spaces to preserve "static" keyword
+ String target = String.join(" ", args).trim();
- // Strip leading "import " if the user typed ":import import java.util.List"
- if (target.toLowerCase().startsWith("import ")) {
- target = target.substring(7).trim();
- }
-
- // Basic validation: must contain a dot (package separator)
- if (!target.contains(".")) {
- terminal.writer().println("Invalid import: '" + target
- + "'. Expected a fully-qualified class or package (e.g. java.util.List or java.util.*).");
+ if (!IMPORTED_ITEM_PATTERN.matcher(target).matches()) {
+ terminal.writer().println("Invalid import definition: '" + target + "'");
terminal.writer().flush();
return null;
}
+ // Strip Java-style semicolons
+ target = target.replace(";", "");
+
try {
engine.execute("import " + target);
terminal.writer().println("==> import " + target);
From 5dfe353abc9ebfe0fe33496e99b9eefb9ad1b117 Mon Sep 17 00:00:00 2001
From: bonampak <14160522+bonampak@users.noreply.github.com>
Date: Thu, 16 Apr 2026 19:42:26 +0200
Subject: [PATCH 34/40] KNOX-3278: Catch Throwable similarly to legacy
GroovySh.
---
.../src/main/java/org/apache/knox/gateway/shell/Shell.java | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java
index 029ed355e2..7d83c41b8a 100644
--- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java
@@ -292,7 +292,8 @@ private static void startInteractiveShell() throws Exception {
} catch (UserInterruptException | EndOfFileException e) {
// Ctrl+C or Ctrl+D cleanly exits the shell
break;
- } catch (Exception e) {
+ } catch (Throwable e) {
+ // Shell should not exit (similar to legacy GroovySh)
terminal.writer().println("Error: " + e.getMessage());
terminal.writer().flush();
}
From cf3fa40fb0302d7278f1a006ff62318d5330a08b Mon Sep 17 00:00:00 2001
From: bonampak <14160522+bonampak@users.noreply.github.com>
Date: Thu, 16 Apr 2026 21:01:50 +0200
Subject: [PATCH 35/40] KNOX-3278: Make LoadCommand handle multi-file loading
as in legacy GroovySh 4.
---
.../gateway/shell/commands/LoadCommand.java | 94 +++++++++++++------
1 file changed, 65 insertions(+), 29 deletions(-)
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoadCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoadCommand.java
index 43652f4ea8..f621d32852 100644
--- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoadCommand.java
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoadCommand.java
@@ -34,7 +34,6 @@
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
-import java.util.stream.Collectors;
/**
* Loads a Groovy script file or URL into the shell and executes it.
@@ -70,39 +69,52 @@ public Object execute(List args) throws Exception {
return null;
}
- // Join args to support paths with spaces (e.g., :load /my path/script.groovy)
- String location = String.join(" ", args).trim();
+ Object lastResult = null;
- String script;
- try {
- script = readScript(location);
- } catch (Exception e) {
- terminal.writer().println("Failed to load '" + location + "': " + e.getMessage());
- terminal.writer().flush();
- return null;
- }
-
- if (script.isEmpty()) {
- terminal.writer().println("Warning: '" + location + "' is empty, nothing to execute.");
- terminal.writer().flush();
- return null;
- }
+ // Iterate over arguments to support multi-file loading
+ // (e.g., :load file1.groovy file2.groovy)
+ for (String location : args) {
+ String script;
+ try {
+ script = readScript(location);
+ } catch (Exception e) {
+ terminal.writer().println("Failed to load '" + location + "': " + e.getMessage());
+ terminal.writer().flush();
+ continue; // Skip to the next file instead of aborting the whole command
+ }
- terminal.writer().println("Loading " + location + " ...");
- terminal.writer().flush();
+ // Legacy feature: strip Unix shebangs (#!/usr/bin/env groovy)
+ if (script.startsWith("#!")) {
+ int newlineIndex = script.indexOf('\n');
+ if (newlineIndex != -1) {
+ script = script.substring(newlineIndex + 1);
+ } else {
+ script = "";
+ }
+ }
- try {
- Object result = engine.execute(script);
- if (result != null) {
- terminal.writer().println("==> " + result);
+ if (script.trim().isEmpty()) {
+ terminal.writer().println("Warning: '" + location + "' is empty, nothing to execute.");
terminal.writer().flush();
+ continue;
}
- return result;
- } catch (Exception e) {
- terminal.writer().println("Error executing script: " + e.getMessage());
+
+ terminal.writer().println("Loading " + location + " ...");
terminal.writer().flush();
- return null;
+
+ try {
+ lastResult = engine.execute(script);
+ if (lastResult != null) {
+ terminal.writer().println("==> " + lastResult);
+ terminal.writer().flush();
+ }
+ } catch (Exception e) {
+ terminal.writer().println("Error executing script '" + location + "': " + e.getMessage());
+ terminal.writer().flush();
+ }
}
+
+ return lastResult;
}
private String readScript(String location) throws IOException {
@@ -127,7 +139,9 @@ private String readScript(String location) throws IOException {
throw new IOException("Path is a directory, not a file: " + path.toAbsolutePath());
}
- return Files.readString(path);
+ try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
+ return readAndSkipShebang(reader);
+ }
}
private boolean isUrl(String location) {
@@ -147,8 +161,30 @@ private String readFromUrl(String urlStr) throws IOException {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(url.openStream(), StandardCharsets.UTF_8))) {
- return reader.lines().collect(Collectors.joining("\n"));
+ return readAndSkipShebang(reader);
+ }
+ }
+
+ private String readAndSkipShebang(BufferedReader reader) throws IOException {
+ String firstLine = reader.readLine();
+ if (firstLine == null) {
+ return "";
+ }
+
+ StringBuilder scriptBuilder = new StringBuilder();
+
+ // If it's not a shebang, preserve the first line
+ if (!firstLine.startsWith("#!")) {
+ scriptBuilder.append(firstLine).append(System.lineSeparator());
}
+
+ // Read the rest of the file
+ String line;
+ while ((line = reader.readLine()) != null) {
+ scriptBuilder.append(line).append(System.lineSeparator());
+ }
+
+ return scriptBuilder.toString();
}
@Override
From eeb8d4ba71da0695c7d17d917335e2c1276539d6 Mon Sep 17 00:00:00 2001
From: bonampak <14160522+bonampak@users.noreply.github.com>
Date: Thu, 16 Apr 2026 22:10:07 +0200
Subject: [PATCH 36/40] KNOX-3278: wrap GroovyEngine completer into
SafeCompleter to handle unexpected exceptions.
---
.../knox/gateway/shell/SafeCompleter.java | 49 +++++++++++++++++++
.../org/apache/knox/gateway/shell/Shell.java | 3 +-
2 files changed, 50 insertions(+), 2 deletions(-)
create mode 100644 gateway-shell/src/main/java/org/apache/knox/gateway/shell/SafeCompleter.java
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/SafeCompleter.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/SafeCompleter.java
new file mode 100644
index 0000000000..fb59eda3ad
--- /dev/null
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/SafeCompleter.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.knox.gateway.shell;
+
+import org.jline.reader.Candidate;
+import org.jline.reader.Completer;
+import org.jline.reader.LineReader;
+import org.jline.reader.ParsedLine;
+
+import java.util.List;
+
+/**
+ * A wrapper to protect JLine from crashing when underlying completers
+ * (like Groovy's reflection completer) throw unexpected JVM exceptions.
+ */
+public class SafeCompleter implements Completer {
+ private final Completer delegate;
+
+ public SafeCompleter(Completer delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public void complete(LineReader reader, ParsedLine line, List candidates) {
+ if (delegate == null) {
+ return;
+ }
+ try {
+ delegate.complete(reader, line, candidates);
+ } catch (Throwable t) {
+ // ignore
+ }
+ }
+}
\ No newline at end of file
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java
index 7d83c41b8a..31da7d7f6e 100644
--- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java
@@ -188,8 +188,7 @@ private static void startInteractiveShell() throws Exception {
Completer combinedCompleter = new AggregateCompleter(
systemRegistry.completer(),
- engine.getScriptCompleter()
- );
+ new SafeCompleter(engine.getScriptCompleter()));
// 5. Build the LineReader
LineReader reader = LineReaderBuilder.builder()
From b50ee89f7ca4d8400ea726b1f5493db3ac57ee75 Mon Sep 17 00:00:00 2001
From: bonampak <14160522+bonampak@users.noreply.github.com>
Date: Thu, 16 Apr 2026 23:38:57 +0200
Subject: [PATCH 37/40] KNOX-3278: correct knoxshell.sh to use launcher and
correctly load classes from the lib directory.
---
gateway-shell-release/home/bin/knoxshell.sh | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/gateway-shell-release/home/bin/knoxshell.sh b/gateway-shell-release/home/bin/knoxshell.sh
index 877441502c..4b54aa177b 100755
--- a/gateway-shell-release/home/bin/knoxshell.sh
+++ b/gateway-shell-release/home/bin/knoxshell.sh
@@ -81,7 +81,7 @@ function main {
checkJava
buildAppJavaOpts
- $JAVA "${APP_JAVA_OPTS[@]}" -Dlog4j.configurationFile=conf/knoxshell-log4j2.xml -javaagent:"$APP_BIN_DIR"/../lib/aspectjweaver.jar -cp "$APP_JAR":lib/* -jar "$APP_JAR" "$@" || exit 1
+ $JAVA "${APP_JAVA_OPTS[@]}" -Dlog4j.configurationFile=conf/knoxshell-log4j2.xml -javaagent:"$APP_BIN_DIR"/../lib/aspectjweaver.jar -cp "$APP_JAR":lib/* -cp "$APP_JAR":lib/* org.apache.knox.gateway.launcher.Launcher "$@" || exit 1
return 0
}
From 6d85aae3552a19ad84c68d007dd3fca3fde17c12 Mon Sep 17 00:00:00 2001
From: bonampak <14160522+bonampak@users.noreply.github.com>
Date: Fri, 17 Apr 2026 10:11:04 +0200
Subject: [PATCH 38/40] KNOX-3278: fix no endline at end of file.
---
.../org/apache/knox/gateway/shell/KnoxShellCommandRegistry.java | 2 +-
.../main/java/org/apache/knox/gateway/shell/SafeCompleter.java | 2 +-
.../org/apache/knox/gateway/shell/commands/ImportCommand.java | 2 +-
.../org/apache/knox/gateway/shell/commands/PurgeCommand.java | 2 +-
.../org/apache/knox/gateway/shell/commands/ShowCommand.java | 2 +-
5 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/KnoxShellCommandRegistry.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/KnoxShellCommandRegistry.java
index a401b67172..a5c077ce7c 100644
--- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/KnoxShellCommandRegistry.java
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/KnoxShellCommandRegistry.java
@@ -105,4 +105,4 @@ private List getCompletersForCommand(String command) {
}
return Collections.singletonList(NullCompleter.INSTANCE);
}
-}
\ No newline at end of file
+}
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/SafeCompleter.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/SafeCompleter.java
index fb59eda3ad..35e9b29097 100644
--- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/SafeCompleter.java
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/SafeCompleter.java
@@ -46,4 +46,4 @@ public void complete(LineReader reader, ParsedLine line, List candida
// ignore
}
}
-}
\ No newline at end of file
+}
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ImportCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ImportCommand.java
index ecee87b95d..a0145af3a5 100644
--- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ImportCommand.java
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ImportCommand.java
@@ -90,4 +90,4 @@ public Object execute(List args) {
terminal.writer().flush();
return null;
}
-}
\ No newline at end of file
+}
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/PurgeCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/PurgeCommand.java
index 92122bc475..1d2564df7c 100644
--- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/PurgeCommand.java
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/PurgeCommand.java
@@ -131,4 +131,4 @@ public List getCompleters() {
Completer subCommandCompleter = new StringsCompleter("variables", "imports", "all");
return Arrays.asList(subCommandCompleter, NullCompleter.INSTANCE);
}
-}
\ No newline at end of file
+}
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ShowCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ShowCommand.java
index aba386fba8..7879868a58 100644
--- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ShowCommand.java
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ShowCommand.java
@@ -108,4 +108,4 @@ public List getCompleters() {
return Arrays.asList(subCommandCompleter, NullCompleter.INSTANCE);
}
-}
\ No newline at end of file
+}
From 0533c3d73cad869b4e5b5b8c9f3cbf74ad1b9990 Mon Sep 17 00:00:00 2001
From: bonampak <14160522+bonampak@users.noreply.github.com>
Date: Fri, 17 Apr 2026 11:46:41 +0200
Subject: [PATCH 39/40] KNOX-3278: refactor Shell.java pt1
---
.../org/apache/knox/gateway/shell/Shell.java | 186 +++++++++++-------
1 file changed, 114 insertions(+), 72 deletions(-)
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java
index 31da7d7f6e..f748a94684 100644
--- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java
@@ -21,6 +21,7 @@
import org.apache.groovy.groovysh.jline.SystemRegistryImpl;
import org.apache.knox.gateway.shell.commands.AbstractKnoxShellCommand;
+import org.apache.knox.gateway.shell.commands.AbstractSQLCommandSupport;
import org.apache.knox.gateway.shell.commands.CSVCommand;
import org.apache.knox.gateway.shell.commands.DataSourceCommand;
import org.apache.knox.gateway.shell.commands.ImportCommand;
@@ -115,41 +116,105 @@ private static void startInteractiveShell() throws Exception {
}
// 3. Instantiate and Map Custom Commands
+ List commands = createCommands(engine, terminal);
+ Map registry = createRegistry(commands);
+
+ Map commandMethods = createCommandMethods(commands);
+ Map commandAliases = createCommandAliases(commands);
+
+ DefaultParser parser = new DefaultParser();
+ // Override default regex to allow '.' as a valid command string
+ // Original: "[:]?[a-zA-Z]+[a-zA-Z0-9_-]*"
+ parser.setRegexCommand("(?:\\.|[:]?[a-zA-Z]+[a-zA-Z0-9_-]*)");
+ Path workDir = Paths.get(System.getProperty("user.dir"));
+ KnoxShellCommandRegistry knoxShellCommandRegistry = new KnoxShellCommandRegistry(commandMethods, commandAliases);
+ SystemRegistry systemRegistry = new SystemRegistryImpl(parser, terminal, () -> workDir, null);
+ systemRegistry.setCommandRegistries(knoxShellCommandRegistry);
+ SystemRegistry.add(systemRegistry);
+
+ // 4. Setup Tab Completers for our custom commands (e.g., ":sql", ":fs")
+ Completer combinedCompleter = new AggregateCompleter(
+ systemRegistry.completer(),
+ new SafeCompleter(engine.getScriptCompleter()));
+
+ // 5. Build the LineReader
+ LineReader reader = LineReaderBuilder.builder()
+ .parser(parser)
+ .terminal(terminal)
+ .completer(combinedCompleter)
+ .variable(LineReader.HISTORY_FILE, Paths.get(System.getProperty("user.home"), ".knoxshell_history"))
+ .build();
+
+ terminal.writer().println("Apache Knox Shell");
+ terminal.writer().println("Type ':help' (':h' or '?') for help, ':exit' or ':quit' (':x' or ':q') to quit.");
+ terminal.writer().flush();
+
+ // 6. Setup Shutdown Hook (Calling closeConnections directly on our object instances)
+ createShutdownHook(commands);
+
+ // 7. The REPL Loop
+ runRepl(reader, terminal, registry, engine);
+ }
+
+ private static List createCommands(GroovyEngine engine, Terminal terminal) {
+ return Arrays.asList(
+ new CSVCommand(engine, terminal),
+ new DataSourceCommand(engine, terminal),
+ new SelectCommand(engine, terminal),
+ new WebHDFSCommand(engine, terminal),
+ new ImportCommand(engine, terminal),
+ new LoadCommand(engine, terminal),
+ new PurgeCommand(engine, terminal),
+ new ShowCommand(engine, terminal)
+ );
+ }
+
+ private static Map createRegistry(List commands) {
Map registry = new HashMap<>();
- CSVCommand csvCmd = new CSVCommand(engine, terminal);
- DataSourceCommand dsCmd = new DataSourceCommand(engine, terminal);
- SelectCommand selectCmd = new SelectCommand(engine, terminal);
- WebHDFSCommand hdfsCmd = new WebHDFSCommand(engine, terminal);
-
- ImportCommand importCmd = new ImportCommand(engine, terminal);
- LoadCommand loadCmd = new LoadCommand(engine, terminal);
- PurgeCommand purgeCommand = new PurgeCommand(engine, terminal);
- ShowCommand showCmd = new ShowCommand(engine, terminal);
-
- registerCommand(registry, importCmd);
- registerCommand(registry, selectCmd);
- registerCommand(registry, dsCmd);
- registerCommand(registry, csvCmd);
- registerCommand(registry, hdfsCmd);
- registerCommand(registry, showCmd);
- registerCommand(registry, loadCmd);
- registerCommand(registry, purgeCommand);
+ if (commands == null || commands.isEmpty()) {
+ return registry;
+ }
- Map commandMethods = new HashMap<>();
- Map commandAliases = new HashMap<>();
+ for (AbstractKnoxShellCommand cmd : commands) {
+ registerCommand(registry, cmd);
+ }
+ return registry;
+ }
- registry.forEach((name, cmd) -> {
- if (name.equals(cmd.getShortcut())) {
- return;
- }
- String shortcut = cmd.getShortcut();
- if (shortcut != null && !shortcut.isEmpty()) {
- commandAliases.put(shortcut, name);
+
+ private static void createShutdownHook(List commands) {
+ if (commands == null || commands.isEmpty()) {
+ return;
+ }
+ Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+ System.out.println("\nClosing any open connections...");
+
+ for (AbstractKnoxShellCommand cmd : commands) {
+ // Check if the command inherits from the SQL base class
+ if (cmd instanceof AbstractSQLCommandSupport) {
+ ((AbstractSQLCommandSupport) cmd).closeConnections();
}
+ }
+ }));
+ }
+
+ private static void registerCommand(Map registry, AbstractKnoxShellCommand cmd) {
+ registry.put(cmd.getName(), cmd);
+ if (cmd.getShortcut() != null && !cmd.getShortcut().isEmpty()) {
+ registry.put(cmd.getShortcut(), cmd);
+ }
+ }
+
+ private static Map createCommandMethods(List commands) {
+ Map commandMethods = new HashMap<>();
- // 2. Put them in the map with the colon-prefixed names
- commandMethods.put(name, new CommandMethods(
+ if (commands == null || commands.isEmpty()) {
+ return commandMethods;
+ }
+
+ for (AbstractKnoxShellCommand cmd : commands) {
+ commandMethods.put(cmd.getName(), new CommandMethods(
(input) -> {
try {
String[] allTokens = input.args();
@@ -171,51 +236,34 @@ private static void startInteractiveShell() throws Exception {
: Collections.singletonList(NullCompleter.INSTANCE);
}
));
- });
-
- DefaultParser parser = new DefaultParser();
- // Override default regex to allow '.' as a valid command string
- // Original: "[:]?[a-zA-Z]+[a-zA-Z0-9_-]*"
- parser.setRegexCommand("(?:\\.|[:]?[a-zA-Z]+[a-zA-Z0-9_-]*)");
- Path workDir = Paths.get(System.getProperty("user.dir"));
- KnoxShellCommandRegistry knoxShellCommandRegistry = new KnoxShellCommandRegistry(commandMethods, commandAliases);
- SystemRegistry systemRegistry = new SystemRegistryImpl(parser, terminal, () -> workDir, null);
- systemRegistry.setCommandRegistries(knoxShellCommandRegistry);
- SystemRegistry.add(systemRegistry);
-
- // 4. Setup Tab Completers
- // StringsCompleter automatically suggests our custom commands (e.g., ":sql", ":fs")
+ }
- Completer combinedCompleter = new AggregateCompleter(
- systemRegistry.completer(),
- new SafeCompleter(engine.getScriptCompleter()));
+ return commandMethods;
+ }
- // 5. Build the LineReader
- LineReader reader = LineReaderBuilder.builder()
- .parser(parser)
- .terminal(terminal)
- .completer(combinedCompleter)
- .variable(LineReader.HISTORY_FILE, Paths.get(System.getProperty("user.home"), ".knoxshell_history"))
- .build();
+ private static Map createCommandAliases(List commands) {
+ Map commandAliases = new HashMap<>();
- terminal.writer().println("Apache Knox Shell");
- terminal.writer().println("Type ':help' (':h' or '?') for help, ':exit' or ':quit' (':x' or ':q') to quit.");
- terminal.writer().flush();
+ if (commands == null || commands.isEmpty()) {
+ return commandAliases;
+ }
- // 6. Setup Shutdown Hook (Calling closeConnections directly on our object instances)
- Runtime.getRuntime().addShutdownHook(new Thread(() -> {
- System.out.println("\nClosing any open connections...");
- dsCmd.closeConnections();
- selectCmd.closeConnections();
- }));
+ for (AbstractKnoxShellCommand cmd : commands) {
+ String shortcut = cmd.getShortcut();
+ if (shortcut != null && !shortcut.isEmpty()) {
+ commandAliases.put(shortcut, cmd.getName());
+ }
+ }
+ return commandAliases;
+ }
- // 7. The REPL Loop
+ private static void runRepl(LineReader reader, Terminal terminal, Map registry, GroovyEngine engine) {
while (true) {
try {
String line = reader.readLine("knox> ");
if (line == null) {
- break;
+ return;
}
String trimmed = line.trim();
@@ -225,7 +273,7 @@ private static void startInteractiveShell() throws Exception {
// --- BUILT-IN COMMANDS ---
if (EXIT_COMMANDS.stream().anyMatch(trimmed::equalsIgnoreCase)) {
- break;
+ return; // Exits the method, allowing main() to finish cleanly
}
if (HELP_COMMANDS.stream().anyMatch(h -> trimmed.equalsIgnoreCase(h) || trimmed.startsWith(h + " "))) {
@@ -290,7 +338,7 @@ private static void startInteractiveShell() throws Exception {
} catch (UserInterruptException | EndOfFileException e) {
// Ctrl+C or Ctrl+D cleanly exits the shell
- break;
+ return;
} catch (Throwable e) {
// Shell should not exit (similar to legacy GroovySh)
terminal.writer().println("Error: " + e.getMessage());
@@ -299,10 +347,4 @@ private static void startInteractiveShell() throws Exception {
}
}
- private static void registerCommand(Map registry, AbstractKnoxShellCommand cmd) {
- registry.put(cmd.getName(), cmd);
- if (cmd.getShortcut() != null && !cmd.getShortcut().isEmpty()) {
- registry.put(cmd.getShortcut(), cmd);
- }
- }
}
From 543095e767fe8ba017d6ed8fa5a6c5aacd13c83f Mon Sep 17 00:00:00 2001
From: bonampak <14160522+bonampak@users.noreply.github.com>
Date: Fri, 17 Apr 2026 13:09:55 +0200
Subject: [PATCH 40/40] KNOX-3278: refactor Shell.java pt2
---
.../org/apache/knox/gateway/shell/Shell.java | 19 +++++++++----------
1 file changed, 9 insertions(+), 10 deletions(-)
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java
index f748a94684..6aff31755b 100644
--- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java
@@ -101,11 +101,11 @@ public static void main(String... args) throws Exception {
}
} else {
// Boot the Interactive JLine 3 REPL
- startInteractiveShell();
+ new Shell().startInteractiveShell();
}
}
- private static void startInteractiveShell() throws Exception {
+ private void startInteractiveShell() throws Exception {
// 1. Build Terminal and Engine
Terminal terminal = TerminalBuilder.builder().system(true).name("KnoxShell").build();
GroovyEngine engine = new GroovyEngine();
@@ -156,7 +156,7 @@ private static void startInteractiveShell() throws Exception {
runRepl(reader, terminal, registry, engine);
}
- private static List createCommands(GroovyEngine engine, Terminal terminal) {
+ private List createCommands(GroovyEngine engine, Terminal terminal) {
return Arrays.asList(
new CSVCommand(engine, terminal),
new DataSourceCommand(engine, terminal),
@@ -169,7 +169,7 @@ private static List createCommands(GroovyEngine engine
);
}
- private static Map createRegistry(List commands) {
+ private Map createRegistry(List commands) {
Map registry = new HashMap<>();
if (commands == null || commands.isEmpty()) {
return registry;
@@ -182,8 +182,7 @@ private static Map createRegistry(List commands) {
+ private void createShutdownHook(List commands) {
if (commands == null || commands.isEmpty()) {
return;
}
@@ -199,14 +198,14 @@ private static void createShutdownHook(List commands)
}));
}
- private static void registerCommand(Map registry, AbstractKnoxShellCommand cmd) {
+ private void registerCommand(Map registry, AbstractKnoxShellCommand cmd) {
registry.put(cmd.getName(), cmd);
if (cmd.getShortcut() != null && !cmd.getShortcut().isEmpty()) {
registry.put(cmd.getShortcut(), cmd);
}
}
- private static Map createCommandMethods(List commands) {
+ private Map createCommandMethods(List commands) {
Map commandMethods = new HashMap<>();
if (commands == null || commands.isEmpty()) {
@@ -241,7 +240,7 @@ private static Map createCommandMethods(List createCommandAliases(List commands) {
+ private Map createCommandAliases(List commands) {
Map commandAliases = new HashMap<>();
if (commands == null || commands.isEmpty()) {
@@ -258,7 +257,7 @@ private static Map createCommandAliases(List registry, GroovyEngine engine) {
+ private void runRepl(LineReader reader, Terminal terminal, Map registry, GroovyEngine engine) {
while (true) {
try {
String line = reader.readLine("knox> ");