This is an automated email from the ASF dual-hosted git repository.
nicoloboschi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pulsar.git
The following commit(s) were added to refs/heads/master by this push:
new a1b8476e40b [feature][cli] Pulsar shell - configs to manage multiple
clusters/tenants - part 4 (#16649)
a1b8476e40b is described below
commit a1b8476e40b6a40e82062915d13e4dc9d7dff9cb
Author: Nicolò Boschi <[email protected]>
AuthorDate: Thu Jul 21 11:07:29 2022 +0200
[feature][cli] Pulsar shell - configs to manage multiple clusters/tenants -
part 4 (#16649)
* Pulsar shell: configs to manage multiple clusters/tenants
* checkstyle
---
bin/pulsar-shell | 17 +-
.../java/org/apache/pulsar/shell/ConfigShell.java | 361 +++++++++++++++++++++
.../apache/pulsar/shell/JCommanderCompleter.java | 117 ++++++-
.../java/org/apache/pulsar/shell/PulsarShell.java | 151 +++++++--
.../apache/pulsar/shell/config/ConfigStore.java | 53 +++
.../pulsar/shell/config/FileConfigStore.java | 152 +++++++++
.../apache/pulsar/shell/config/package-info.java | 19 ++
.../org/apache/pulsar/shell/ConfigShellTest.java | 144 ++++++++
.../pulsar/shell/JCommanderCompleterTest.java | 2 +-
.../org/apache/pulsar/shell/PulsarShellTest.java | 2 +-
10 files changed, 957 insertions(+), 61 deletions(-)
diff --git a/bin/pulsar-shell b/bin/pulsar-shell
index f2aa9a56a20..b0ad7cec84d 100755
--- a/bin/pulsar-shell
+++ b/bin/pulsar-shell
@@ -34,19 +34,6 @@ BINDIR=$(dirname "$PRG")
export PULSAR_HOME=`cd -P $BINDIR/..;pwd`
. "$PULSAR_HOME/bin/pulsar-admin-common.sh"
OPTS="-Dorg.jline.terminal.jansi=false $OPTS"
+DEFAULT_CONFIG="-Dpulsar.shell.config.default=$PULSAR_CLIENT_CONF"
-#Change to PULSAR_HOME to support relative paths
-cd "$PULSAR_HOME"
-DEFAULT_SHELL_ARGS="--config $PULSAR_CLIENT_CONF"
-PASSED_SHELL_ARGS=""
-while [[ $# -gt 0 ]]
-do
- key="$1"
- if [[ "$key" == "-c" || "$key" == "--config" ]]; then
- DEFAULT_SHELL_ARGS=""
- fi
- PASSED_SHELL_ARGS="$PASSED_SHELL_ARGS $key"
- shift
-done
-
-exec $JAVA $OPTS org.apache.pulsar.shell.PulsarShell $DEFAULT_SHELL_ARGS
$PASSED_SHELL_ARGS
\ No newline at end of file
+exec $JAVA $OPTS $DEFAULT_CONFIG org.apache.pulsar.shell.PulsarShell "$@"
\ No newline at end of file
diff --git
a/pulsar-client-tools/src/main/java/org/apache/pulsar/shell/ConfigShell.java
b/pulsar-client-tools/src/main/java/org/apache/pulsar/shell/ConfigShell.java
new file mode 100644
index 00000000000..48bc423336d
--- /dev/null
+++ b/pulsar-client-tools/src/main/java/org/apache/pulsar/shell/ConfigShell.java
@@ -0,0 +1,361 @@
+/**
+ * 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.pulsar.shell;
+
+import static org.apache.pulsar.shell.config.ConfigStore.DEFAULT_CONFIG;
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.ParameterException;
+import com.beust.jcommander.Parameters;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringReader;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.stream.Collectors;
+import lombok.Getter;
+import lombok.SneakyThrows;
+import org.apache.commons.io.IOUtils;
+import org.apache.pulsar.shell.config.ConfigStore;
+
+/**
+ * Shell commands to manage shell configurations.
+ */
+@Parameters(commandDescription = "Manage Pulsar shell configurations.")
+public class ConfigShell implements ShellCommandsProvider {
+
+
+ @Getter
+ @Parameters
+ public static class Params {
+
+ @Parameter(names = {"-h", "--help"}, help = true, description = "Show
this help.")
+ boolean help;
+ }
+
+ private interface RunnableWithResult {
+ boolean run() throws Exception;
+ }
+
+ private JCommander jcommander;
+ private Params params;
+ private final PulsarShell pulsarShell;
+ private final Map<String, RunnableWithResult> commands = new HashMap<>();
+ private final ConfigStore configStore;
+ private final ObjectMapper writer = new
ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
+ @Getter
+ private String currentConfig = DEFAULT_CONFIG;
+
+ public ConfigShell(PulsarShell pulsarShell) {
+ this.configStore = pulsarShell.getConfigStore();
+ this.pulsarShell = pulsarShell;
+ }
+
+ @Override
+ public String getName() {
+ return "config";
+ }
+
+ @Override
+ public String getServiceUrl() {
+ return null;
+ }
+
+ @Override
+ public String getAdminUrl() {
+ return null;
+ }
+
+ @Override
+ public void setupState(Properties properties) {
+
+ this.params = new Params();
+ this.jcommander = new JCommander();
+ jcommander.addObject(params);
+
+ commands.put("list", new CmdConfigList());
+ commands.put("create", new CmdConfigCreate());
+ commands.put("update", new CmdConfigUpdate());
+ commands.put("delete", new CmdConfigDelete());
+ commands.put("use", new CmdConfigUse());
+ commands.put("view", new CmdConfigView());
+ commands.forEach((k, v) -> jcommander.addCommand(k, v));
+ }
+
+ @Override
+ public void cleanupState(Properties properties) {
+ setupState(properties);
+ }
+
+ @Override
+ public JCommander getJCommander() {
+ return jcommander;
+ }
+
+ @Override
+ public boolean runCommand(String[] args) throws Exception {
+ try {
+ jcommander.parse(args);
+
+ if (params.help) {
+ jcommander.usage();
+ return true;
+ }
+
+ String chosenCommand = jcommander.getParsedCommand();
+ final RunnableWithResult command = commands.get(chosenCommand);
+ if (command == null) {
+ jcommander.usage();
+ return false;
+ }
+ return command.run();
+ } catch (Throwable e) {
+ jcommander.getConsole().println(e.getMessage());
+ String chosenCommand = jcommander.getParsedCommand();
+ if (e instanceof ParameterException) {
+ try {
+ jcommander.getUsageFormatter().usage(chosenCommand);
+ } catch (ParameterException noCmd) {
+ e.printStackTrace();
+ }
+ } else {
+ e.printStackTrace();
+ }
+ return false;
+ }
+ }
+
+ @Parameters(commandDescription = "List configurations")
+ private class CmdConfigList implements RunnableWithResult {
+
+ @Override
+ @SneakyThrows
+ public boolean run() {
+ print(configStore
+ .listConfigs()
+ .stream()
+ .map(e -> formatEntry(e))
+ .collect(Collectors.toList())
+ );
+ return true;
+ }
+
+ private String formatEntry(ConfigStore.ConfigEntry entry) {
+ final String name = entry.getName();
+ if (name.equals(currentConfig)) {
+ return name + " (*)";
+ }
+ return name;
+ }
+ }
+
+ @Parameters(commandDescription = "Use the configuration for next commands")
+ private class CmdConfigUse implements RunnableWithResult {
+ @Parameter(description = "Name of the config", required = true)
+ @JCommanderCompleter.ParameterCompleter(type =
JCommanderCompleter.ParameterCompleter.Type.CONFIGS)
+ private String name;
+
+ @Override
+ @SneakyThrows
+ public boolean run() {
+ final ConfigStore.ConfigEntry config = configStore.getConfig(name);
+ if (config == null) {
+ print("Config " + name + " not found");
+ return false;
+ }
+ final String value = config.getValue();
+ currentConfig = name;
+ final Properties properties = new Properties();
+ properties.load(new StringReader(value));
+ pulsarShell.reload(properties);
+ configStore.setLastUsed(name);
+ return true;
+ }
+ }
+
+ @Parameters(commandDescription = "View configuration")
+ private class CmdConfigView implements RunnableWithResult {
+ @Parameter(description = "Name of the config", required = true)
+ @JCommanderCompleter.ParameterCompleter(type =
JCommanderCompleter.ParameterCompleter.Type.CONFIGS)
+ private String name;
+
+ @Override
+ @SneakyThrows
+ public boolean run() {
+ final ConfigStore.ConfigEntry config =
configStore.getConfig(this.name);
+ if (config == null) {
+ print("Config " + name + " not found");
+ return false;
+ }
+ print(config.getValue());
+ return true;
+ }
+ }
+
+ @Parameters(commandDescription = "Delete a configuration")
+ private class CmdConfigDelete implements RunnableWithResult {
+ @Parameter(description = "Name of the config", required = true)
+ @JCommanderCompleter.ParameterCompleter(type =
JCommanderCompleter.ParameterCompleter.Type.CONFIGS)
+ private String name;
+
+ @Override
+ @SneakyThrows
+ public boolean run() {
+ if (DEFAULT_CONFIG.equals(name)) {
+ print("'" + name + "' can't be deleted.");
+ return false;
+ }
+ if (currentConfig != null && currentConfig.equals(name)) {
+ print("'" + name + "' is currently used and it can't be
deleted.");
+ return false;
+ }
+ configStore.deleteConfig(name);
+ return true;
+ }
+ }
+
+ @Parameters(commandDescription = "Create a new configuration.")
+ private class CmdConfigCreate extends CmdConfigPut {
+
+ @Override
+ @SneakyThrows
+ boolean verifyCondition() {
+ final boolean exists = configStore.getConfig(name) != null;
+ if (exists) {
+ print("Config '" + name + "' already exists.");
+ return false;
+ }
+ return true;
+ }
+ }
+
+ @Parameters(commandDescription = "Update an existing configuration.")
+ private class CmdConfigUpdate extends CmdConfigPut {
+
+ @Override
+ @SneakyThrows
+ boolean verifyCondition() {
+ if (DEFAULT_CONFIG.equals(name)) {
+ print("'" + name + "' can't be updated.");
+ return false;
+ }
+ final boolean exists = configStore.getConfig(name) != null;
+ if (!exists) {
+ print("Config '" + name + "' does not exist.");
+ return false;
+ }
+ return true;
+ }
+ }
+
+ private abstract class CmdConfigPut implements RunnableWithResult {
+
+ @Parameter(description = "Configuration name", required = true)
+ @JCommanderCompleter.ParameterCompleter(type =
JCommanderCompleter.ParameterCompleter.Type.CONFIGS)
+ protected String name;
+
+ @Parameter(names = {"--url"}, description = "URL of the config")
+ protected String url;
+
+ @Parameter(names = {"--file"}, description = "File path of the config")
+ @JCommanderCompleter.ParameterCompleter(type =
JCommanderCompleter.ParameterCompleter.Type.FILES)
+ protected String file;
+
+ @Parameter(names = {"--value"}, description = "Inline value of the
config")
+ protected String inlineValue;
+
+ @Override
+ @SneakyThrows
+ public boolean run() {
+ if (!verifyCondition()) {
+ return false;
+ }
+ final String value;
+ if (inlineValue != null) {
+ if (inlineValue.startsWith("base64:")) {
+ final byte[] bytes =
Base64.getDecoder().decode(inlineValue.substring("base64:".length()));
+ value = new String(bytes, StandardCharsets.UTF_8);
+ } else {
+ value = inlineValue;
+ }
+ } else if (file != null) {
+ final File f = new File(file);
+ if (!f.exists()) {
+ print("File " + f.getAbsolutePath() + " not found.");
+ return false;
+ }
+ value = new String(Files.readAllBytes(f.toPath()),
StandardCharsets.UTF_8);
+ } else if (url != null) {
+ final ByteArrayOutputStream bout = new ByteArrayOutputStream();
+ try {
+ try (InputStream in =
URI.create(url).toURL().openStream()) {
+ IOUtils.copy(in, bout);
+ }
+ } catch (IOException | IllegalArgumentException e) {
+ print("Failed to download configuration: " +
e.getMessage());
+ return false;
+ }
+ value = new String(bout.toByteArray(), StandardCharsets.UTF_8);
+ } else {
+ print("At least one between --file, --url or --value is
required.");
+ return false;
+ }
+
+ configStore.putConfig(new ConfigStore.ConfigEntry(name, value));
+ if (currentConfig.equals(name)) {
+ final Properties properties = new Properties();
+ properties.load(new StringReader(value));
+ pulsarShell.reload(properties);
+ }
+ return true;
+ }
+
+ abstract boolean verifyCondition();
+ }
+
+
+ <T> void print(List<T> items) {
+ for (T item : items) {
+ print(item);
+ }
+ }
+
+ <T> void print(T item) {
+ try {
+ if (item instanceof String) {
+ jcommander.getConsole().println((String) item);
+ } else {
+
jcommander.getConsole().println(writer.writeValueAsString(item));
+ }
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+}
\ No newline at end of file
diff --git
a/pulsar-client-tools/src/main/java/org/apache/pulsar/shell/JCommanderCompleter.java
b/pulsar-client-tools/src/main/java/org/apache/pulsar/shell/JCommanderCompleter.java
index d3aaf7297b0..6ef608173fd 100644
---
a/pulsar-client-tools/src/main/java/org/apache/pulsar/shell/JCommanderCompleter.java
+++
b/pulsar-client-tools/src/main/java/org/apache/pulsar/shell/JCommanderCompleter.java
@@ -18,9 +18,14 @@
*/
package org.apache.pulsar.shell;
+import static java.lang.annotation.ElementType.FIELD;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.ParameterDescription;
import com.beust.jcommander.WrappedParameter;
+import java.io.File;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -28,9 +33,16 @@ import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.SneakyThrows;
import org.apache.pulsar.admin.cli.CmdBase;
+import org.apache.pulsar.shell.config.ConfigStore;
import org.jline.builtins.Completers;
+import org.jline.reader.Candidate;
import org.jline.reader.Completer;
+import org.jline.reader.LineReader;
+import org.jline.reader.ParsedLine;
import org.jline.reader.impl.completer.NullCompleter;
import org.jline.reader.impl.completer.StringsCompleter;
@@ -39,40 +51,58 @@ import org.jline.reader.impl.completer.StringsCompleter;
*/
public class JCommanderCompleter {
+ @AllArgsConstructor
+ @Getter
+ public static class ShellContext {
+ private final ConfigStore configStore;
+ }
+
private JCommanderCompleter() {
}
public static List<Completer> createCompletersForCommand(String program,
- JCommander
command) {
+ JCommander
command,
+ ShellContext
shellContext) {
command.setProgramName(program);
return createCompletersForCommand(Collections.emptyList(),
command,
- Arrays.asList(NullCompleter.INSTANCE));
+ Arrays.asList(NullCompleter.INSTANCE),
+ shellContext);
}
private static List<Completer> createCompletersForCommand(List<Completer>
preCompleters,
JCommander
command,
- List<Completer>
postCompleters) {
+ List<Completer>
postCompleters,
+ ShellContext
shellContext) {
List<Completer> all = new ArrayList<>();
- addCompletersForCommand(preCompleters, postCompleters, all, command);
+ addCompletersForCommand(preCompleters, postCompleters, all, command,
shellContext);
return all;
}
private static void addCompletersForCommand(List<Completer> preCompleters,
List<Completer> postCompleters,
List<Completer> result,
- JCommander command) {
+ JCommander command,
+ ShellContext shellContext) {
final Collection<Completers.OptDesc> options;
final Map<String, JCommander> subCommands;
+ final ParameterDescription mainParameterValue;
if (command.getObjects().get(0) instanceof CmdBase) {
CmdBase cmdBase = (CmdBase) command.getObjects().get(0);
subCommands = cmdBase.getJcommander().getCommands();
- options =
cmdBase.getJcommander().getParameters().stream().map(JCommanderCompleter::createOptionDescriptors)
+ mainParameterValue = cmdBase.getJcommander().getMainParameter() ==
null ? null :
+ cmdBase.getJcommander().getMainParameterValue();
+ options = cmdBase.getJcommander().getParameters()
+ .stream()
+ .map(option -> createOptionDescriptors(option,
shellContext))
.collect(Collectors.toList());
} else {
subCommands = command.getCommands();
- options =
command.getParameters().stream().map(JCommanderCompleter::createOptionDescriptors)
+ mainParameterValue = command.getMainParameter() == null ? null :
command.getMainParameterValue();
+ options = command.getParameters()
+ .stream()
+ .map(option -> createOptionDescriptors(option,
shellContext))
.collect(Collectors.toList());
}
@@ -86,7 +116,13 @@ public class JCommanderCompleter {
completersChain.add(new Completers.OptionCompleter(options,
preCompleters.size() + 1 + j));
}
for (Map.Entry<String, JCommander> subCommand :
subCommands.entrySet()) {
- addCompletersForCommand(completersChain, postCompleters,
result, subCommand.getValue());
+ addCompletersForCommand(completersChain, postCompleters,
result, subCommand.getValue(), shellContext);
+ }
+ if (mainParameterValue != null) {
+ final Completer customCompleter =
getCustomCompleter(mainParameterValue, shellContext);
+ if (customCompleter != null) {
+ completersChain.add(customCompleter);
+ }
}
completersChain.addAll(postCompleters);
result.add(new OptionStrictArgumentCompleter(completersChain));
@@ -94,14 +130,9 @@ public class JCommanderCompleter {
}
- private static Completers.OptDesc
createOptionDescriptors(ParameterDescription param) {
- Completer valueCompleter = null;
- boolean isBooleanArg = param.getObject() instanceof Boolean ||
param.getDefault() instanceof Boolean
- ||
param.getObject().getClass().isAssignableFrom(Boolean.class);
- if (!isBooleanArg) {
- valueCompleter = Completers.AnyCompleter.INSTANCE;
- }
-
+ @SneakyThrows
+ private static Completers.OptDesc
createOptionDescriptors(ParameterDescription param, ShellContext shellContext) {
+ Completer valueCompleter = getCompleter(param, shellContext);
final WrappedParameter parameter = param.getParameter();
String shortOption = null;
String longOption = null;
@@ -116,4 +147,58 @@ public class JCommanderCompleter {
return new Completers.OptDesc(shortOption, longOption,
param.getDescription(), valueCompleter);
}
+ @SneakyThrows
+ private static Completer getCompleter(ParameterDescription param,
ShellContext shellContext) {
+
+ Completer valueCompleter = null;
+ boolean isBooleanArg = param.getObject() instanceof Boolean ||
param.getDefault() instanceof Boolean
+ ||
param.getObject().getClass().isAssignableFrom(Boolean.class);
+ if (!isBooleanArg) {
+ valueCompleter = getCustomCompleter(param, shellContext);
+ if (valueCompleter == null) {
+ valueCompleter = Completers.AnyCompleter.INSTANCE;
+ }
+ }
+ return valueCompleter;
+ }
+
+ @SneakyThrows
+ private static Completer getCustomCompleter(ParameterDescription param,
ShellContext shellContext) {
+ Completer valueCompleter = null;
+ final Field reflField =
param.getParameterized().getClass().getDeclaredField("field");
+ reflField.setAccessible(true);
+ final Field field = (Field) reflField.get(param.getParameterized());
+ final ParameterCompleter parameterCompleter =
field.getAnnotation(ParameterCompleter.class);
+ if (parameterCompleter != null) {
+ final ParameterCompleter.Type completer =
parameterCompleter.type();
+ if (completer == ParameterCompleter.Type.FILES) {
+ valueCompleter = new Completers.FilesCompleter(new
File(System.getProperty("user.dir")));
+ } else if (completer == ParameterCompleter.Type.CONFIGS) {
+ valueCompleter = new Completer() {
+ @Override
+ @SneakyThrows
+ public void complete(LineReader reader, ParsedLine line,
List<Candidate> candidates) {
+ new
StringsCompleter(shellContext.configStore.listConfigs()
+
.stream().map(ConfigStore.ConfigEntry::getName).collect(Collectors.toList()))
+ .complete(reader, line, candidates);
+ }
+ };
+ }
+ }
+ return valueCompleter;
+ }
+
+ @Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
+ @Target({ FIELD })
+ public @interface ParameterCompleter {
+
+ enum Type {
+ FILES,
+ CONFIGS;
+ }
+
+ Type type();
+
+ }
+
}
diff --git
a/pulsar-client-tools/src/main/java/org/apache/pulsar/shell/PulsarShell.java
b/pulsar-client-tools/src/main/java/org/apache/pulsar/shell/PulsarShell.java
index 2b72c0574eb..d8c0ee61878 100644
--- a/pulsar-client-tools/src/main/java/org/apache/pulsar/shell/PulsarShell.java
+++ b/pulsar-client-tools/src/main/java/org/apache/pulsar/shell/PulsarShell.java
@@ -21,10 +21,12 @@ package org.apache.pulsar.shell;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;
import java.io.BufferedReader;
-import java.io.FileInputStream;
+import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
+import java.io.StringReader;
import java.net.URI;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
@@ -32,14 +34,19 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
+import java.util.Scanner;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
+import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringSubstitutor;
+import org.apache.pulsar.shell.config.ConfigStore;
+import org.apache.pulsar.shell.config.FileConfigStore;
import org.jline.reader.Completer;
import org.jline.reader.LineReader;
import org.jline.reader.LineReaderBuilder;
+import org.jline.reader.impl.DefaultParser;
import org.jline.reader.impl.completer.AggregateCompleter;
import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder;
@@ -53,6 +60,7 @@ import org.jline.utils.InfoCmp;
public class PulsarShell {
private static final String EXIT_MESSAGE = "Goodbye!";
+ private static final String PROPERTY_PULSAR_SHELL_DIR =
"shellHistoryDirectory";
private static final String PROPERTY_PERSIST_HISTORY_ENABLED =
"shellHistoryPersistEnabled";
private static final String PROPERTY_PERSIST_HISTORY_PATH =
"shellHistoryPersistPath";
private static final String CHECKMARK = new
String(Character.toChars(0x2714));
@@ -98,22 +106,32 @@ public class PulsarShell {
+ " Each command must be separated by a newline.")
String filename;
- @Parameter(names = {"-e", "--exit-on-error"}, description = "If true,
the shell will be interrupted "
+ @Parameter(names = {"--fail-on-error"}, description = "If true, the
shell will be interrupted "
+ "if a command throws an exception.")
- boolean exitOnError;
+ boolean failOnError;
@Parameter(names = {"-"}, description = "Read commands from the
standard input.")
boolean readFromStdin;
+ @Parameter(names = {"-e", "--execute-command"}, description = "Execute
this command and exit.")
+ String inlineCommand;
+
@Parameter(names = {"-np", "--no-progress"}, description = "Display
raw output of the commands without the "
+ "fancy progress visualization.")
boolean noProgress;
}
- private final Properties properties;
+ private Properties properties;
+ @Getter
+ private final ConfigStore configStore;
+ private final File pulsarShellDir;
private final JCommander mainCommander;
private final MainOptions mainOptions;
+ private JCommander shellCommander;
private final String[] args;
+ private Function<Map<String, ShellCommandsProvider>,
InteractiveLineReader> readerBuilder;
+ private InteractiveLineReader reader;
+ private ConfigShell configShell;
public PulsarShell(String args[]) throws IOException {
this(args, new Properties());
@@ -132,19 +150,57 @@ public class PulsarShell {
exit(1);
throw new IllegalArgumentException(e);
}
+
+ pulsarShellDir = computePulsarShellFile();
+ Files.createDirectories(pulsarShellDir.toPath());
+ System.out.println(String.format("Using directory: %s",
pulsarShellDir.getAbsolutePath()));
+
+ ConfigStore.ConfigEntry defaultConfig = null;
+ String configFile;
+
if (mainOptions.configFile != null) {
- String configFile = mainOptions.configFile;
- try (FileInputStream fis = new FileInputStream(configFile)) {
- properties.load(fis);
- }
+ configFile = mainOptions.configFile;
+ } else {
+ configFile = System.getProperty("pulsar.shell.config.default");
+ }
+ if (configFile != null) {
+ final String defaultConfigValue =
+ new String(Files.readAllBytes(new
File(configFile).toPath()), StandardCharsets.UTF_8);
+ defaultConfig = new
ConfigStore.ConfigEntry(ConfigStore.DEFAULT_CONFIG, defaultConfigValue);
+ }
+
+ configStore = new FileConfigStore(
+ Paths.get(pulsarShellDir.getAbsolutePath(),
"configs.json").toFile(),
+ defaultConfig);
+
+ final ConfigStore.ConfigEntry lastUsed = configStore.getLastUsed();
+ if (lastUsed != null) {
+ properties.load(new StringReader(lastUsed.getValue()));
+ } else if (defaultConfig != null) {
+ properties.load(new StringReader(defaultConfig.getValue()));
}
this.args = args;
}
+ private static File computePulsarShellFile() {
+ String dir = System.getProperty(PROPERTY_PULSAR_SHELL_DIR, null);
+ if (dir == null) {
+ return Paths.get(System.getProperty("user.home"),
".pulsar-shell").toFile();
+ } else {
+ return new File(dir);
+ }
+ }
+
public static void main(String[] args) throws Exception {
new PulsarShell(args).run();
}
+ public void reload(Properties properties) throws Exception {
+ this.properties = properties;
+ final Map<String, ShellCommandsProvider> providersMap =
registerProviders(shellCommander, properties);
+ reader = readerBuilder.apply(providersMap);
+ }
+
public void run() throws Exception {
System.setProperty("org.jline.terminal.dumb", "true");
final Terminal terminal = TerminalBuilder.builder().build();
@@ -152,13 +208,14 @@ public class PulsarShell {
List<Completer> completers = new ArrayList<>();
String serviceUrl = "";
String adminUrl = "";
+ final JCommanderCompleter.ShellContext shellContext = new
JCommanderCompleter.ShellContext(configStore);
for (ShellCommandsProvider provider : providersMap.values()) {
provider.setupState(properties);
final JCommander jCommander = provider.getJCommander();
if (jCommander != null) {
jCommander.createDescriptions();
completers.addAll(JCommanderCompleter
- .createCompletersForCommand(provider.getName(),
jCommander));
+ .createCompletersForCommand(provider.getName(),
jCommander, shellContext));
}
final String providerServiceUrl = provider.getServiceUrl();
@@ -175,6 +232,7 @@ public class PulsarShell {
LineReaderBuilder readerBuilder = LineReaderBuilder.builder()
.terminal(terminal)
+ .parser(new DefaultParser().eofOnUnclosedQuote(true))
.completer(completer)
.variable(LineReader.INDENTATION, 2)
.option(LineReader.Option.INSERT_BRACKET, true);
@@ -183,14 +241,25 @@ public class PulsarShell {
LineReader reader = readerBuilder.build();
final String welcomeMessage =
- String.format("Welcome to Pulsar shell!\n %s: %s\n %s:
%s\n\n "
- + "Type 'help' to get started or try the
autocompletion (TAB button).\n",
+ String.format("Welcome to Pulsar shell!\n %s: %s\n %s:
%s\n\n"
+ + "Type %s to get started or try the
autocompletion (TAB button).\n"
+ + "Type %s or %s to end the shell
session.\n",
new
AttributedStringBuilder().style(AttributedStyle.BOLD).append("Service
URL").toAnsi(),
serviceUrl,
new
AttributedStringBuilder().style(AttributedStyle.BOLD).append("Admin
URL").toAnsi(),
- adminUrl);
+ adminUrl,
+ new
AttributedStringBuilder().style(AttributedStyle.BOLD).append("help").toAnsi(),
+ new
AttributedStringBuilder().style(AttributedStyle.BOLD).append("exit").toAnsi(),
+ new
AttributedStringBuilder().style(AttributedStyle.BOLD).append("quit").toAnsi());
output(welcomeMessage, terminal);
- final String prompt = createPrompt(getHostFromUrl(serviceUrl));
+ String promptMessage;
+ if (configShell != null && configShell.getCurrentConfig() != null)
{
+ promptMessage = String.format("%s(%s)",
+ configShell.getCurrentConfig(),
getHostFromUrl(serviceUrl));
+ } else {
+ promptMessage = getHostFromUrl(serviceUrl);
+ }
+ final String prompt = createPrompt(promptMessage);
return new InteractiveLineReader() {
@Override
public String readLine() {
@@ -209,11 +278,9 @@ public class PulsarShell {
final boolean isPersistHistoryEnabled =
Boolean.parseBoolean(properties.getProperty(
PROPERTY_PERSIST_HISTORY_ENABLED, "true"));
if (isPersistHistoryEnabled) {
- final String persistHistoryPath = properties
- .getProperty(PROPERTY_PERSIST_HISTORY_PATH,
Paths.get(System.getProperty("user.home"),
-
".pulsar-shell.history").toFile().getAbsolutePath());
- readerBuilder
- .variable(LineReader.HISTORY_FILE, persistHistoryPath);
+ final String historyPath =
Paths.get(pulsarShellDir.getAbsolutePath(), "history")
+ .toFile().getAbsolutePath();
+ readerBuilder.variable(LineReader.HISTORY_FILE, historyPath);
}
}
@@ -245,27 +312,24 @@ public class PulsarShell {
public void run(Function<Map<String, ShellCommandsProvider>,
InteractiveLineReader> readerBuilder,
Function<Map<String, ShellCommandsProvider>, Terminal>
terminalBuilder) throws Exception {
+ this.readerBuilder = readerBuilder;
/**
* Options read from the shell session
*/
- final JCommander shellCommander = new JCommander();
+ shellCommander = new JCommander();
final ShellOptions shellOptions = new ShellOptions();
shellCommander.addObject(shellOptions);
final Map<String, ShellCommandsProvider> providersMap =
registerProviders(shellCommander, properties);
- final InteractiveLineReader reader = readerBuilder.apply(providersMap);
+ reader = readerBuilder.apply(providersMap);
final Terminal terminal = terminalBuilder.apply(providersMap);
final Map<String, String> variables = System.getenv();
CommandReader commandReader;
CommandsInfo commandsInfo = null;
- if (mainOptions.readFromStdin && mainOptions.filename != null) {
- throw new IllegalArgumentException("Cannot use stdin and
-f/--filename option at same time");
- }
- boolean isNonInteractiveMode = mainOptions.filename != null ||
mainOptions.readFromStdin;
-
+ boolean isNonInteractiveMode = isNonInteractiveMode();
if (isNonInteractiveMode) {
final List<String> lines;
if (mainOptions.filename != null) {
@@ -273,10 +337,18 @@ public class PulsarShell {
.stream()
.filter(PulsarShell::filterLine)
.collect(Collectors.toList());
- } else {
+ } else if (mainOptions.readFromStdin) {
try (BufferedReader stdinReader = new BufferedReader(new
InputStreamReader(System.in))) {
lines =
stdinReader.lines().filter(PulsarShell::filterLine).collect(Collectors.toList());
}
+ } else {
+ lines = new ArrayList<>();
+ try (Scanner scanner = new
Scanner(mainOptions.inlineCommand);) {
+ while (scanner.hasNextLine()) {
+ String line = scanner.nextLine().trim();
+ lines.add(line);
+ }
+ }
}
if (!mainOptions.noProgress) {
commandsInfo = new CommandsInfo();
@@ -350,7 +422,7 @@ public class PulsarShell {
} catch (Throwable t) {
t.printStackTrace(terminal.writer());
} finally {
- final boolean willExitWithError = mainOptions.exitOnError &&
!commandOk;
+ final boolean willExitWithError = mainOptions.failOnError &&
!commandOk;
if (commandsInfo != null && !willExitWithError) {
commandsInfo.executingCommand = null;
commandsInfo.executedCommands.add(new
CommandsInfo.ExecutedCommandInfo(line, commandOk));
@@ -359,13 +431,32 @@ public class PulsarShell {
pulsarShellCommandsProvider.cleanupState(properties);
}
- if (mainOptions.exitOnError && !commandOk) {
+ if (mainOptions.failOnError && !commandOk) {
exit(1);
return;
}
}
}
+ private boolean isNonInteractiveMode() {
+ boolean commandOk = true;
+ if (mainOptions.inlineCommand != null) {
+ if (mainOptions.readFromStdin
+ || mainOptions.filename != null) {
+ commandOk = false;
+ }
+ } else if (mainOptions.readFromStdin
+ && mainOptions.filename != null) {
+ commandOk = false;
+ }
+
+ if (!commandOk) {
+ throw new IllegalArgumentException("Cannot use stdin,
-e/--execute-command "
+ + "and -f/--filename option at the same time");
+ }
+ return mainOptions.filename != null || mainOptions.readFromStdin ||
mainOptions.inlineCommand != null;
+ }
+
private void printExecutingCommands(Terminal terminal,
CommandsInfo commandsInfo,
boolean printExecuted) {
@@ -462,6 +553,10 @@ public class PulsarShell {
final Map<String, ShellCommandsProvider> providerMap = new HashMap<>();
registerProvider(createAdminShell(properties), commander, providerMap);
registerProvider(createClientShell(properties), commander,
providerMap);
+ if (configShell == null) {
+ configShell = new ConfigShell(this);
+ }
+ registerProvider(configShell, commander, providerMap);
return providerMap;
}
diff --git
a/pulsar-client-tools/src/main/java/org/apache/pulsar/shell/config/ConfigStore.java
b/pulsar-client-tools/src/main/java/org/apache/pulsar/shell/config/ConfigStore.java
new file mode 100644
index 00000000000..cd994cc5354
--- /dev/null
+++
b/pulsar-client-tools/src/main/java/org/apache/pulsar/shell/config/ConfigStore.java
@@ -0,0 +1,53 @@
+/**
+ * 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.pulsar.shell.config;
+
+import java.io.IOException;
+import java.util.List;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * Shell configurations store layer.
+ */
+public interface ConfigStore {
+
+ String DEFAULT_CONFIG = "default";
+ @Data
+ @AllArgsConstructor
+ @NoArgsConstructor
+ class ConfigEntry {
+ String name;
+ String value;
+ }
+
+
+ void putConfig(ConfigEntry entry) throws IOException;
+
+ ConfigEntry getConfig(String name) throws IOException;
+
+ void deleteConfig(String name) throws IOException;
+
+ List<ConfigEntry> listConfigs() throws IOException;
+
+ void setLastUsed(String name) throws IOException;
+
+ ConfigEntry getLastUsed() throws IOException;
+}
diff --git
a/pulsar-client-tools/src/main/java/org/apache/pulsar/shell/config/FileConfigStore.java
b/pulsar-client-tools/src/main/java/org/apache/pulsar/shell/config/FileConfigStore.java
new file mode 100644
index 00000000000..f7d558dba98
--- /dev/null
+++
b/pulsar-client-tools/src/main/java/org/apache/pulsar/shell/config/FileConfigStore.java
@@ -0,0 +1,152 @@
+/**
+ * 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.pulsar.shell.config;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.exc.MismatchedInputException;
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Scanner;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * File based configurations store.
+ *
+ * All the configurations are stored in a single file in JSON format.
+ */
+public class FileConfigStore implements ConfigStore {
+
+ @Data
+ @NoArgsConstructor
+ public static class FileConfig {
+ private LinkedHashMap<String, ConfigEntry> configs = new
LinkedHashMap<>();
+ private String last;
+ }
+
+ private final ObjectMapper mapper = new ObjectMapper();
+ private final File file;
+ private final ConfigEntry defaultConfig;
+ private FileConfig fileConfig;
+
+ public FileConfigStore(File file, ConfigEntry defaultConfig) throws
IOException {
+ this.file = file;
+ if (file.exists()) {
+ read();
+ } else {
+ fileConfig = new FileConfig();
+ }
+ if (defaultConfig != null) {
+ this.defaultConfig = new ConfigEntry(defaultConfig.getName(),
defaultConfig.getValue());
+ cleanupValue(this.defaultConfig);
+ } else {
+ this.defaultConfig = null;
+ }
+ }
+
+ private void read() throws IOException {
+ try (final BufferedInputStream buffered = new BufferedInputStream(new
FileInputStream(file));) {
+ try {
+ fileConfig = mapper.readValue(buffered, FileConfig.class);
+ } catch (MismatchedInputException mismatchedInputException) {
+ fileConfig = new FileConfig();
+ }
+ }
+ }
+
+ private void write() throws IOException {
+ try (final BufferedOutputStream bufferedOutputStream = new
BufferedOutputStream(new FileOutputStream(file));) {
+ mapper.writeValue(bufferedOutputStream, fileConfig);
+ }
+ }
+
+ @Override
+ public void putConfig(ConfigEntry entry) throws IOException {
+ if (DEFAULT_CONFIG.equals(entry.getName())) {
+ throw new IllegalArgumentException("'" + DEFAULT_CONFIG + "' can't
be modified.");
+ }
+ cleanupValue(entry);
+ fileConfig.configs.put(entry.getName(), entry);
+ write();
+ }
+
+ private static void cleanupValue(ConfigEntry entry) {
+ StringBuilder builder = new StringBuilder();
+ try (Scanner scanner = new Scanner(entry.getValue());) {
+ while (scanner.hasNextLine()) {
+ String line = scanner.nextLine().trim();
+ if (line.startsWith("#")) {
+ continue;
+ }
+ builder.append(line);
+ builder.append(System.lineSeparator());
+ }
+ }
+ entry.setValue(builder.toString());
+ }
+
+ @Override
+ public ConfigEntry getConfig(String name) {
+ if (DEFAULT_CONFIG.equals(name)) {
+ return defaultConfig;
+ }
+ return fileConfig.configs.get(name);
+ }
+
+ @Override
+ public void deleteConfig(String name) throws IOException{
+ if (DEFAULT_CONFIG.equals(name)) {
+ throw new IllegalArgumentException("'" + DEFAULT_CONFIG + "' can't
be deleted.");
+ }
+ final ConfigEntry old = fileConfig.configs.remove(name);
+ if (old != null) {
+ write();
+ }
+ }
+
+ @Override
+ public List<ConfigEntry> listConfigs() {
+ List<ConfigEntry> all = new ArrayList<>(fileConfig.configs.values());
+ if (defaultConfig != null) {
+ all.add(0, defaultConfig);
+ }
+ return all;
+ }
+
+ @Override
+ public void setLastUsed(String name) throws IOException {
+ fileConfig.last = name;
+ write();
+ }
+
+ @Override
+ public ConfigEntry getLastUsed() throws IOException {
+ if (fileConfig.last != null) {
+ return getConfig(fileConfig.last);
+ }
+ return null;
+ }
+}
diff --git
a/pulsar-client-tools/src/main/java/org/apache/pulsar/shell/config/package-info.java
b/pulsar-client-tools/src/main/java/org/apache/pulsar/shell/config/package-info.java
new file mode 100644
index 00000000000..7f4c6f3bcd0
--- /dev/null
+++
b/pulsar-client-tools/src/main/java/org/apache/pulsar/shell/config/package-info.java
@@ -0,0 +1,19 @@
+/**
+ * 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.pulsar.shell.config;
diff --git
a/pulsar-client-tools/src/test/java/org/apache/pulsar/shell/ConfigShellTest.java
b/pulsar-client-tools/src/test/java/org/apache/pulsar/shell/ConfigShellTest.java
new file mode 100644
index 00000000000..f5b22af299f
--- /dev/null
+++
b/pulsar-client-tools/src/test/java/org/apache/pulsar/shell/ConfigShellTest.java
@@ -0,0 +1,144 @@
+/**
+ * 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.pulsar.shell;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+import com.beust.jcommander.internal.Console;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Properties;
+import org.apache.pulsar.shell.config.ConfigStore;
+import org.apache.pulsar.shell.config.FileConfigStore;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+public class ConfigShellTest {
+
+ private PulsarShell pulsarShell;
+ private ConfigShell configShell;
+ private final List<String> output = new ArrayList<>();
+
+ @BeforeMethod(alwaysRun = true)
+ public void before() throws Exception {
+
+ pulsarShell = spy(mock(PulsarShell.class));
+ doNothing().when(pulsarShell).reload(any());
+ final Path tempJson = Files.createTempFile("pulsar-shell", ".json");
+
+ when(pulsarShell.getConfigStore()).thenReturn(
+ new FileConfigStore(tempJson.toFile(),
+ new
ConfigStore.ConfigEntry(ConfigStore.DEFAULT_CONFIG,
"#comment\ndefault-config=true")));
+ configShell = new ConfigShell(pulsarShell);
+ configShell.setupState(new Properties());
+
+ configShell.getJCommander().setConsole(new Console() {
+ @Override
+ public void print(String msg) {
+ System.out.print("got: " + msg);
+ output.add(msg);
+ }
+
+ @Override
+ public void println(String msg) {
+ System.out.println("got: " + msg);
+ output.add(msg);
+ }
+
+ @Override
+ public char[] readPassword(boolean echoInput) {
+ return new char[0];
+ }
+ });
+
+ }
+
+ @Test
+ public void testDefault() throws Exception {
+ assertTrue(configShell.runCommand(new String[]{"list"}));
+ assertEquals(output, Arrays.asList("default (*)"));
+ output.clear();
+ assertTrue(configShell.runCommand(new String[]{"view", "default"}));
+ assertEquals(output.get(0), "default-config=true\n");
+ output.clear();
+
+ final Path newClientConf = Files.createTempFile("client", ".conf");
+ assertFalse(configShell.runCommand(new String[]{"create", "default",
+ "--file", newClientConf.toFile().getAbsolutePath()}));
+ assertEquals(output, Arrays.asList("Config 'default' already
exists."));
+ output.clear();
+
+ assertFalse(configShell.runCommand(new String[]{"update", "default",
+ "--file", newClientConf.toFile().getAbsolutePath()}));
+ assertEquals(output, Arrays.asList("'default' can't be updated."));
+ output.clear();
+
+ assertFalse(configShell.runCommand(new String[]{"delete", "default"}));
+ assertEquals(output, Arrays.asList("'default' can't be deleted."));
+ }
+
+ @Test
+ public void test() throws Exception {
+ final Path newClientConf = Files.createTempFile("client", ".conf");
+
+ final byte[] content = ("webServiceUrl=http://localhost:8081/\n" +
+
"brokerServiceUrl=pulsar://localhost:6651/\n").getBytes(StandardCharsets.UTF_8);
+ Files.write(newClientConf, content);
+ assertTrue(configShell.runCommand(new String[]{"create", "myclient",
+ "--file", newClientConf.toFile().getAbsolutePath()}));
+ assertTrue(output.isEmpty());
+ output.clear();
+
+ assertNull(pulsarShell.getConfigStore().getLastUsed());
+
+ assertTrue(configShell.runCommand(new String[]{"use", "myclient"}));
+ assertTrue(output.isEmpty());
+ output.clear();
+ assertEquals(pulsarShell.getConfigStore().getLastUsed(),
pulsarShell.getConfigStore()
+ .getConfig("myclient"));
+
+ verify(pulsarShell).reload(any());
+
+ assertTrue(configShell.runCommand(new String[]{"list"}));
+ assertEquals(output, Arrays.asList("default", "myclient (*)"));
+ output.clear();
+
+ assertFalse(configShell.runCommand(new String[]{"delete",
"myclient"}));
+ assertEquals(output, Arrays.asList("'myclient' is currently used and
it can't be deleted."));
+ output.clear();
+
+ assertTrue(configShell.runCommand(new String[]{"update", "myclient",
+ "--file", newClientConf.toFile().getAbsolutePath()}));
+ assertTrue(output.isEmpty());
+ verify(pulsarShell, times(2)).reload(any());
+ }
+}
\ No newline at end of file
diff --git
a/pulsar-client-tools/src/test/java/org/apache/pulsar/shell/JCommanderCompleterTest.java
b/pulsar-client-tools/src/test/java/org/apache/pulsar/shell/JCommanderCompleterTest.java
index 254e237ac6e..455bfd6b444 100644
---
a/pulsar-client-tools/src/test/java/org/apache/pulsar/shell/JCommanderCompleterTest.java
+++
b/pulsar-client-tools/src/test/java/org/apache/pulsar/shell/JCommanderCompleterTest.java
@@ -44,7 +44,7 @@ public class JCommanderCompleterTest {
private void createAndCheckCompleters(AdminShell shell, String
mainCommand) {
final List<Completer> completers =
JCommanderCompleter.createCompletersForCommand(mainCommand,
- shell.getJCommander());
+ shell.getJCommander(), null);
assertFalse(completers.isEmpty());
for (Completer completer : completers) {
assertTrue(completer instanceof OptionStrictArgumentCompleter);
diff --git
a/pulsar-client-tools/src/test/java/org/apache/pulsar/shell/PulsarShellTest.java
b/pulsar-client-tools/src/test/java/org/apache/pulsar/shell/PulsarShellTest.java
index 0a6f4fcdf41..1102845ada2 100644
---
a/pulsar-client-tools/src/test/java/org/apache/pulsar/shell/PulsarShellTest.java
+++
b/pulsar-client-tools/src/test/java/org/apache/pulsar/shell/PulsarShellTest.java
@@ -189,7 +189,7 @@ public class PulsarShellTest {
final String shellFile = Thread.currentThread()
.getContextClassLoader().getResource("test-shell-file-error").getFile();
- final TestPulsarShell testPulsarShell = new TestPulsarShell(new
String[]{"-f", shellFile, "-e"},
+ final TestPulsarShell testPulsarShell = new TestPulsarShell(new
String[]{"-f", shellFile, "--fail-on-error"},
props, pulsarAdminBuilder);
try {
testPulsarShell.run((a) -> linereader, (a) -> terminal);