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);

Reply via email to