This is an automated email from the ASF dual-hosted git repository.

technoboy 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 69c45ad5300 [improve][cli] PIP-343: Use picocli instead of jcommander 
in pulsar-perf (#22303)
69c45ad5300 is described below

commit 69c45ad5300e36a62a923b8eaa58aab99c6e02fb
Author: crossoverJie <crossover...@gmail.com>
AuthorDate: Fri Mar 22 09:12:37 2024 +0800

    [improve][cli] PIP-343: Use picocli instead of jcommander in pulsar-perf 
(#22303)
    
    Co-authored-by: Zixuan Liu <node...@gmail.com>
---
 .../cli/converters/ByteUnitToLongConverter.java    | 39 ---------
 .../pulsar/cli/converters/ByteConversionTest.java  |  9 +-
 pulsar-testclient/pom.xml                          |  4 +-
 .../proxy/socket/client/PerformanceClient.java     | 65 +++++++--------
 .../apache/pulsar/testclient/BrokerMonitor.java    | 30 +++----
 .../testclient/CmdGenerateDocumentation.java       | 67 +++++++++------
 .../pulsar/testclient/LoadSimulationClient.java    | 34 ++++----
 .../testclient/LoadSimulationController.java       | 68 +++++++--------
 .../pulsar/testclient/ManagedLedgerWriter.java     | 57 ++++++-------
 .../testclient/PerformanceBaseArguments.java       | 59 +++++++------
 .../pulsar/testclient/PerformanceConsumer.java     | 65 ++++++++-------
 .../pulsar/testclient/PerformanceProducer.java     | 96 +++++++++++-----------
 .../pulsar/testclient/PerformanceReader.java       | 19 +++--
 .../testclient/PerformanceTopicListArguments.java  | 10 ++-
 .../pulsar/testclient/PerformanceTransaction.java  | 45 +++++-----
 ...or.java => PositiveNumberParameterConvert.java} | 15 ++--
 .../pulsar/testclient/GenerateDocumentionTest.java | 37 +++++++++
 17 files changed, 377 insertions(+), 342 deletions(-)

diff --git 
a/pulsar-cli-utils/src/main/java/org/apache/pulsar/cli/converters/ByteUnitToLongConverter.java
 
b/pulsar-cli-utils/src/main/java/org/apache/pulsar/cli/converters/ByteUnitToLongConverter.java
deleted file mode 100644
index 6170fb489d4..00000000000
--- 
a/pulsar-cli-utils/src/main/java/org/apache/pulsar/cli/converters/ByteUnitToLongConverter.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * 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.cli.converters;
-
-import static org.apache.pulsar.cli.ValueValidationUtil.emptyCheck;
-import com.beust.jcommander.converters.BaseConverter;
-
-public class ByteUnitToLongConverter extends BaseConverter<Long> {
-
-    public ByteUnitToLongConverter(String optionName) {
-        super(optionName);
-    }
-
-    @Override
-    public Long convert(String argStr) {
-        return parseBytes(argStr);
-    }
-
-    Long parseBytes(String argStr) {
-        emptyCheck(getOptionName(), argStr);
-        return ByteUnitUtil.validateSizeString(argStr);
-    }
-}
diff --git 
a/pulsar-cli-utils/src/test/java/org/apache/pulsar/cli/converters/ByteConversionTest.java
 
b/pulsar-cli-utils/src/test/java/org/apache/pulsar/cli/converters/ByteConversionTest.java
index 283e94bfb9c..6e7a2e6d7e3 100644
--- 
a/pulsar-cli-utils/src/test/java/org/apache/pulsar/cli/converters/ByteConversionTest.java
+++ 
b/pulsar-cli-utils/src/test/java/org/apache/pulsar/cli/converters/ByteConversionTest.java
@@ -21,6 +21,7 @@ package org.apache.pulsar.cli.converters;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertThrows;
 import org.apache.pulsar.cli.converters.picocli.ByteUnitToIntegerConverter;
+import org.apache.pulsar.cli.converters.picocli.ByteUnitToLongConverter;
 import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
 import picocli.CommandLine.TypeConversionException;
@@ -59,8 +60,8 @@ public class ByteConversionTest {
     }
 
     @Test(dataProvider = "successfulByteUnitUtilTestCases")
-    public void testSuccessfulByteUnitToLongConverter(String input, long 
expected) {
-        ByteUnitToLongConverter converter = new 
ByteUnitToLongConverter("optionName");
+    public void testSuccessfulByteUnitToLongConverter(String input, long 
expected) throws Exception{
+        ByteUnitToLongConverter converter = new ByteUnitToLongConverter();
         assertEquals(converter.convert(input), Long.valueOf(expected));
     }
 
@@ -78,8 +79,8 @@ public class ByteConversionTest {
 
     @Test(dataProvider = "failingByteUnitUtilTestCases")
     public void testFailedByteUnitToLongConverter(String input) {
-        ByteUnitToLongConverter converter = new 
ByteUnitToLongConverter("optionName");
-        assertThrows(IllegalArgumentException.class, () -> 
converter.convert(input));
+        ByteUnitToLongConverter converter = new ByteUnitToLongConverter();
+        assertThrows(TypeConversionException.class, () -> 
converter.convert(input));
     }
 
     @Test(dataProvider = "failingByteUnitUtilTestCases")
diff --git a/pulsar-testclient/pom.xml b/pulsar-testclient/pom.xml
index db2b84356e6..ecc12b2e563 100644
--- a/pulsar-testclient/pom.xml
+++ b/pulsar-testclient/pom.xml
@@ -97,8 +97,8 @@
                </dependency>
 
                <dependency>
-                       <groupId>com.beust</groupId>
-                       <artifactId>jcommander</artifactId>
+                       <groupId>info.picocli</groupId>
+                       <artifactId>picocli</artifactId>
                        <scope>compile</scope>
                </dependency>
 
diff --git 
a/pulsar-testclient/src/main/java/org/apache/pulsar/proxy/socket/client/PerformanceClient.java
 
b/pulsar-testclient/src/main/java/org/apache/pulsar/proxy/socket/client/PerformanceClient.java
index 596eb8d2c28..9d95d0b74a2 100644
--- 
a/pulsar-testclient/src/main/java/org/apache/pulsar/proxy/socket/client/PerformanceClient.java
+++ 
b/pulsar-testclient/src/main/java/org/apache/pulsar/proxy/socket/client/PerformanceClient.java
@@ -20,10 +20,6 @@ package org.apache.pulsar.proxy.socket.client;
 
 import static org.apache.commons.lang3.StringUtils.isBlank;
 import static org.apache.commons.lang3.StringUtils.isNotBlank;
-import com.beust.jcommander.JCommander;
-import com.beust.jcommander.Parameter;
-import com.beust.jcommander.ParameterException;
-import com.beust.jcommander.Parameters;
 import com.google.common.util.concurrent.RateLimiter;
 import io.netty.util.concurrent.DefaultThreadFactory;
 import java.io.FileInputStream;
@@ -55,13 +51,18 @@ import org.apache.pulsar.client.api.AuthenticationFactory;
 import org.apache.pulsar.common.naming.TopicName;
 import org.apache.pulsar.testclient.IMessageFormatter;
 import org.apache.pulsar.testclient.PerfClientUtils;
-import org.apache.pulsar.testclient.PositiveNumberParameterValidator;
+import org.apache.pulsar.testclient.PositiveNumberParameterConvert;
 import org.apache.pulsar.testclient.utils.PaddingDecimalFormat;
 import org.eclipse.jetty.util.ssl.SslContextFactory;
 import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
 import org.eclipse.jetty.websocket.client.WebSocketClient;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Option;
+import picocli.CommandLine.ParameterException;
+import picocli.CommandLine.Parameters;
 
 public class PerformanceClient {
 
@@ -70,87 +71,87 @@ public class PerformanceClient {
     private static final LongAdder totalMessagesSent = new LongAdder();
     private static final LongAdder totalBytesSent = new LongAdder();
     private static IMessageFormatter messageFormatter = null;
-    private JCommander jc;
+    private CommandLine commander;
 
-    @Parameters(commandDescription = "Test pulsar websocket producer 
performance.")
+    @Command(description = "Test pulsar websocket producer performance.")
     static class Arguments {
 
-        @Parameter(names = { "-h", "--help" }, description = "Help message", 
help = true)
+        @Option(names = { "-h", "--help" }, description = "Help message", help 
= true)
         boolean help;
 
-        @Parameter(names = { "-cf", "--conf-file" }, description = 
"Configuration file")
+        @Option(names = { "-cf", "--conf-file" }, description = "Configuration 
file")
         public String confFile;
 
-        @Parameter(names = { "-u", "--proxy-url" }, description = "Pulsar 
Proxy URL, e.g., \"ws://localhost:8080/\"")
+        @Option(names = { "-u", "--proxy-url" }, description = "Pulsar Proxy 
URL, e.g., \"ws://localhost:8080/\"")
         public String proxyURL;
 
-        @Parameter(description = "persistent://tenant/ns/my-topic", required = 
true)
+        @Parameters(description = "persistent://tenant/ns/my-topic", arity = 
"1")
         public List<String> topics;
 
-        @Parameter(names = { "-r", "--rate" }, description = "Publish rate 
msg/s across topics")
+        @Option(names = { "-r", "--rate" }, description = "Publish rate msg/s 
across topics")
         public int msgRate = 100;
 
-        @Parameter(names = { "-s", "--size" }, description = "Message size in 
byte")
+        @Option(names = { "-s", "--size" }, description = "Message size in 
byte")
         public int msgSize = 1024;
 
-        @Parameter(names = { "-t", "--num-topic" }, description = "Number of 
topics",
-                validateWith = PositiveNumberParameterValidator.class)
+        @Option(names = { "-t", "--num-topic" }, description = "Number of 
topics",
+                converter = PositiveNumberParameterConvert.class
+        )
         public int numTopics = 1;
 
-        @Parameter(names = { "--auth_plugin" }, description = "Authentication 
plugin class name", hidden = true)
+        @Option(names = { "--auth_plugin" }, description = "Authentication 
plugin class name", hidden = true)
         public String deprecatedAuthPluginClassName;
 
-        @Parameter(names = { "--auth-plugin" }, description = "Authentication 
plugin class name")
+        @Option(names = { "--auth-plugin" }, description = "Authentication 
plugin class name")
         public String authPluginClassName;
 
-        @Parameter(
+        @Option(
             names = { "--auth-params" },
             description = "Authentication parameters, whose format is 
determined by the implementation "
                     + "of method `configure` in authentication plugin class, 
for example \"key1:val1,key2:val2\" "
                     + "or \"{\"key1\":\"val1\",\"key2\":\"val2\"}\".")
         public String authParams;
 
-        @Parameter(names = { "-m",
+        @Option(names = { "-m",
                 "--num-messages" }, description = "Number of messages to 
publish in total. If <= 0, it will keep"
                 + " publishing")
         public long numMessages = 0;
 
-        @Parameter(names = { "-f", "--payload-file" }, description = "Use 
payload from a file instead of empty buffer")
+        @Option(names = { "-f", "--payload-file" }, description = "Use payload 
from a file instead of empty buffer")
         public String payloadFilename = null;
 
-        @Parameter(names = { "-e", "--payload-delimiter" },
+        @Option(names = { "-e", "--payload-delimiter" },
                 description = "The delimiter used to split lines when using 
payload from a file")
         // here escaping \n since default value will be printed with the help 
text
         public String payloadDelimiter = "\\n";
 
-        @Parameter(names = { "-fp", "--format-payload" },
+        @Option(names = { "-fp", "--format-payload" },
                 description = "Format %i as a message index in the stream from 
producer and/or %t as the timestamp"
                         + " nanoseconds")
         public boolean formatPayload = false;
 
-        @Parameter(names = {"-fc", "--format-class"}, description = "Custom 
Formatter class name")
+        @Option(names = {"-fc", "--format-class"}, description = "Custom 
Formatter class name")
         public String formatterClass = 
"org.apache.pulsar.testclient.DefaultMessageFormatter";
 
-        @Parameter(names = { "-time",
+        @Option(names = { "-time",
                 "--test-duration" }, description = "Test duration in secs. If 
<= 0, it will keep publishing")
         public long testTime = 0;
     }
 
     public Arguments loadArguments(String[] args) {
         Arguments arguments = new Arguments();
-        jc = new JCommander(arguments);
-        jc.setProgramName("pulsar-perf websocket-producer");
-
+        commander = new CommandLine(arguments);
+        commander.setCommandName("pulsar-perf websocket-producer");
         try {
-            jc.parse(args);
+            commander.parseArgs(args);
         } catch (ParameterException e) {
             System.out.println(e.getMessage());
-            jc.usage();
+            commander.usage(commander.getOut());
             PerfClientUtils.exit(1);
         }
 
         if (arguments.help) {
-            jc.usage();
+            commander.usage(commander.getOut());
             PerfClientUtils.exit(1);
         }
 
@@ -160,7 +161,7 @@ public class PerformanceClient {
 
         if (arguments.topics.size() != 1) {
             System.err.println("Only one topic name is allowed");
-            jc.usage();
+            commander.usage(commander.getOut());
             PerfClientUtils.exit(1);
         }
 
@@ -171,7 +172,7 @@ public class PerformanceClient {
                 prop.load(new FileInputStream(arguments.confFile));
             } catch (IOException e) {
                 log.error("Error in loading config file");
-                jc.usage();
+                commander.usage(commander.getOut());
                 PerfClientUtils.exit(1);
             }
 
diff --git 
a/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/BrokerMonitor.java
 
b/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/BrokerMonitor.java
index a3e5a14a416..d195e8fd456 100644
--- 
a/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/BrokerMonitor.java
+++ 
b/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/BrokerMonitor.java
@@ -20,10 +20,6 @@ package org.apache.pulsar.testclient;
 
 import static 
org.apache.pulsar.broker.loadbalance.extensions.ExtensibleLoadManagerImpl.BROKER_LOAD_DATA_STORE_TOPIC;
 import static 
org.apache.pulsar.broker.resources.LoadBalanceResources.BROKER_TIME_AVERAGE_BASE_PATH;
-import com.beust.jcommander.JCommander;
-import com.beust.jcommander.Parameter;
-import com.beust.jcommander.ParameterException;
-import com.beust.jcommander.Parameters;
 import com.google.gson.Gson;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -50,6 +46,11 @@ import org.apache.zookeeper.Watcher;
 import org.apache.zookeeper.ZooKeeper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Option;
+import picocli.CommandLine.ParameterException;
+import picocli.CommandLine.ScopeType;
 
 /**
  * Monitors brokers and prints to the console information about their system 
resource usages, their topic and bundle
@@ -434,17 +435,18 @@ public class BrokerMonitor {
         }
     }
 
-    // JCommander arguments class.
-    @Parameters(commandDescription = "Monitors brokers and prints to the 
console information about their system "
-            + "resource usages, \ntheir topic and bundle counts, their message 
rates, and other metrics.")
+    // picocli arguments class.
+    @Command(description = "Monitors brokers and prints to the console 
information about their system "
+            + "resource usages, \ntheir topic and bundle counts, their message 
rates, and other metrics.",
+            showDefaultValues = true, scope = ScopeType.INHERIT)
     private static class Arguments {
-        @Parameter(names = { "-h", "--help" }, description = "Help message", 
help = true)
+        @Option(names = { "-h", "--help" }, description = "Help message", help 
= true)
         boolean help;
 
-        @Parameter(names = { "--connect-string" }, description = "Zookeeper or 
broker connect string", required = true)
+        @Option(names = { "--connect-string" }, description = "Zookeeper or 
broker connect string", required = true)
         public String connectString = null;
 
-        @Parameter(names = { "--extensions" }, description = "true to monitor 
Load Balance Extensions.")
+        @Option(names = { "--extensions" }, description = "true to monitor 
Load Balance Extensions.")
         boolean extensions = false;
     }
 
@@ -546,14 +548,14 @@ public class BrokerMonitor {
      */
     public static void main(String[] args) throws Exception {
         final Arguments arguments = new Arguments();
-        final JCommander jc = new JCommander(arguments);
-        jc.setProgramName("pulsar-perf monitor-brokers");
+        final CommandLine commander = new CommandLine(arguments);
+        commander.setCommandName("pulsar-perf monitor-brokers");
 
         try {
-            jc.parse(args);
+            commander.parseArgs(args);
         } catch (ParameterException e) {
             System.out.println(e.getMessage());
-            jc.usage();
+            commander.usage(commander.getOut());
             PerfClientUtils.exit(1);
         }
 
diff --git 
a/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/CmdGenerateDocumentation.java
 
b/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/CmdGenerateDocumentation.java
index e3aca988655..6ff0ab296a6 100644
--- 
a/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/CmdGenerateDocumentation.java
+++ 
b/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/CmdGenerateDocumentation.java
@@ -18,46 +18,47 @@
  */
 package org.apache.pulsar.testclient;
 
-import com.beust.jcommander.JCommander;
-import com.beust.jcommander.Parameter;
-import com.beust.jcommander.ParameterDescription;
-import com.beust.jcommander.ParameterException;
-import com.beust.jcommander.Parameters;
 import java.lang.reflect.Constructor;
 import java.util.ArrayList;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import lombok.extern.slf4j.Slf4j;
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Option;
+import picocli.CommandLine.ParameterException;
+import picocli.CommandLine.ScopeType;
 
 @Slf4j
 public class CmdGenerateDocumentation {
 
-    @Parameters(commandDescription = "Generate documentation automatically.")
+    @Command(description = "Generate documentation automatically.", 
showDefaultValues = true, scope = ScopeType.INHERIT)
     static class Arguments {
 
-        @Parameter(names = {"-h", "--help"}, description = "Help message", 
help = true)
+        @Option(names = {"-h", "--help"}, description = "Help message", help = 
true)
         boolean help;
 
-        @Parameter(names = {"-n", "--command-names"}, description = "List of 
command names")
+        @Option(names = {"-n", "--command-names"}, description = "List of 
command names")
         private List<String> commandNames = new ArrayList<>();
 
     }
 
     public static void main(String[] args) throws Exception {
         final Arguments arguments = new Arguments();
-        final JCommander jc = new JCommander(arguments);
-        jc.setProgramName("pulsar-perf gen-doc");
+        CommandLine commander = new CommandLine(arguments);
+        commander.setCommandName("pulsar-perf gen-doc");
         try {
-            jc.parse(args);
+            commander.parseArgs(args);
         } catch (ParameterException e) {
             System.out.println(e.getMessage());
-            jc.usage();
+            commander.usage(commander.getOut());
             PerfClientUtils.exit(1);
         }
 
+
         if (arguments.help) {
-            jc.usage();
+            commander.usage(commander.getOut());
             PerfClientUtils.exit(1);
         }
 
@@ -80,38 +81,54 @@ public class CmdGenerateDocumentation {
             Class<?> clazz = entry.getValue();
             Constructor<?> constructor = clazz.getDeclaredConstructor();
             constructor.setAccessible(true);
-            jc.addCommand(cmd, constructor.newInstance());
+            commander.addSubcommand(cmd, constructor.newInstance());
         }
 
         if (arguments.commandNames.size() == 0) {
-            for (Map.Entry<String, JCommander> cmd : 
jc.getCommands().entrySet()) {
-                generateDocument(cmd.getKey(), jc);
+            for (Map.Entry<String, CommandLine> cmd : 
commander.getSubcommands().entrySet()) {
+                generateDocument(cmd.getKey(), commander);
             }
         } else {
             for (String commandName : arguments.commandNames) {
-                generateDocument(commandName, jc);
+                generateDocument(commandName, commander);
             }
         }
     }
 
-    private static String generateDocument(String module, JCommander 
parentCmd) {
+    private static String generateDocument(String module, CommandLine 
parentCmd) {
         StringBuilder sb = new StringBuilder();
-        JCommander cmd = parentCmd.getCommands().get(module);
+        CommandLine cmd = parentCmd.getSubcommands().get(module);
         sb.append("## ").append(module).append("\n\n");
-        
sb.append(parentCmd.getUsageFormatter().getCommandDescription(module)).append("\n");
+        sb.append(getCommandDescription(cmd)).append("\n");
         sb.append("\n\n```shell\n")
                 .append("$ pulsar-perf ").append(module).append(" [options]")
                 .append("\n```");
         sb.append("\n\n");
         sb.append("|Flag|Description|Default|\n");
         sb.append("|---|---|---|\n");
-        List<ParameterDescription> options = cmd.getParameters();
-        options.stream().filter(ele -> 
!ele.getParameterAnnotation().hidden()).forEach((option) ->
-                sb.append("| `").append(option.getNames())
-                        .append("` | 
").append(option.getDescription().replace("\n", " "))
-                        .append("|").append(option.getDefault()).append("|\n")
+        List<CommandLine.Model.OptionSpec> options = 
cmd.getCommandSpec().options();
+        options.stream().filter(ele -> !ele.hidden()).forEach((option) ->
+                sb.append("| `").append(String.join(", ", option.names()))
+                        .append("` | 
").append(getOptionDescription(option).replace("\n", " "))
+                        
.append("|").append(option.defaultValueString()).append("|\n")
         );
         System.out.println(sb.toString());
         return sb.toString();
     }
+
+    public static String getCommandDescription(CommandLine commandLine) {
+        String[] description = 
commandLine.getCommandSpec().usageMessage().description();
+        if (description != null && description.length != 0) {
+            return description[0];
+        }
+        return "";
+    }
+
+    public static String getOptionDescription(CommandLine.Model.OptionSpec 
optionSpec) {
+        String[] description = optionSpec.description();
+        if (description != null && description.length != 0) {
+            return description[0];
+        }
+        return "";
+    }
 }
diff --git 
a/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/LoadSimulationClient.java
 
b/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/LoadSimulationClient.java
index 982c71ce6a5..42d2f0dd514 100644
--- 
a/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/LoadSimulationClient.java
+++ 
b/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/LoadSimulationClient.java
@@ -18,10 +18,6 @@
  */
 package org.apache.pulsar.testclient;
 
-import com.beust.jcommander.JCommander;
-import com.beust.jcommander.Parameter;
-import com.beust.jcommander.ParameterException;
-import com.beust.jcommander.Parameters;
 import com.google.common.util.concurrent.RateLimiter;
 import io.netty.util.concurrent.DefaultThreadFactory;
 import java.io.DataInputStream;
@@ -37,7 +33,7 @@ import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Function;
-import org.apache.pulsar.cli.converters.ByteUnitToLongConverter;
+import org.apache.pulsar.cli.converters.picocli.ByteUnitToLongConverter;
 import org.apache.pulsar.client.admin.PulsarAdmin;
 import org.apache.pulsar.client.admin.PulsarAdminException;
 import org.apache.pulsar.client.api.Consumer;
@@ -48,6 +44,11 @@ import org.apache.pulsar.client.api.PulsarClient;
 import org.apache.pulsar.client.api.SizeUnit;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Option;
+import picocli.CommandLine.ParameterException;
+import picocli.CommandLine.ScopeType;
 
 /**
  * LoadSimulationClient is used to simulate client load by maintaining 
producers and consumers for topics. Instances of
@@ -170,19 +171,20 @@ public class LoadSimulationClient {
         }
     }
 
-    // JCommander arguments for starting a LoadSimulationClient.
-    @Parameters(commandDescription = "Simulate client load by maintaining 
producers and consumers for topics.")
+    // picocli arguments for starting a LoadSimulationClient.
+    @Command(description = "Simulate client load by maintaining producers and 
consumers for topics.",
+            showDefaultValues = true, scope = ScopeType.INHERIT)
     private static class MainArguments {
-        @Parameter(names = { "-h", "--help" }, description = "Help message", 
help = true)
+        @Option(names = { "-h", "--help" }, description = "Help message", help 
= true)
         boolean help;
 
-        @Parameter(names = { "--port" }, description = "Port to listen on for 
controller", required = true)
+        @Option(names = { "--port" }, description = "Port to listen on for 
controller", required = true)
         public int port;
 
-        @Parameter(names = { "--service-url" }, description = "Pulsar Service 
URL", required = true)
+        @Option(names = { "--service-url" }, description = "Pulsar Service 
URL", required = true)
         public String serviceURL;
 
-        @Parameter(names = { "-ml", "--memory-limit", }, description = 
"Configure the Pulsar client memory limit "
+        @Option(names = { "-ml", "--memory-limit", }, description = "Configure 
the Pulsar client memory limit "
             + "(eg: 32M, 64M)", converter = ByteUnitToLongConverter.class)
         public long memoryLimit = 0L;
     }
@@ -310,7 +312,7 @@ public class LoadSimulationClient {
     private static final MessageListener<byte[]> ackListener = 
Consumer::acknowledgeAsync;
 
     /**
-     * Create a LoadSimulationClient with the given JCommander arguments.
+     * Create a LoadSimulationClient with the given picocli arguments.
      *
      * @param arguments
      *            Arguments to configure this from.
@@ -341,13 +343,13 @@ public class LoadSimulationClient {
      */
     public static void main(String[] args) throws Exception {
         final MainArguments mainArguments = new MainArguments();
-        final JCommander jc = new JCommander(mainArguments);
-        jc.setProgramName("pulsar-perf simulation-client");
+        CommandLine commander = new CommandLine(mainArguments);
+        commander.setCommandName("pulsar-perf simulation-client");
         try {
-            jc.parse(args);
+            commander.parseArgs(args);
         } catch (ParameterException e) {
             System.out.println(e.getMessage());
-            jc.usage();
+            commander.usage(commander.getOut());
             PerfClientUtils.exit(1);
         }
         PerfClientUtils.printJVMInformation(log);
diff --git 
a/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/LoadSimulationController.java
 
b/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/LoadSimulationController.java
index e967ba9e517..94186c581eb 100644
--- 
a/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/LoadSimulationController.java
+++ 
b/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/LoadSimulationController.java
@@ -20,10 +20,6 @@ package org.apache.pulsar.testclient;
 
 import static 
org.apache.pulsar.broker.resources.LoadBalanceResources.BUNDLE_DATA_BASE_PATH;
 import static 
org.apache.pulsar.broker.resources.LoadBalanceResources.RESOURCE_QUOTA_BASE_PATH;
-import com.beust.jcommander.JCommander;
-import com.beust.jcommander.Parameter;
-import com.beust.jcommander.ParameterException;
-import com.beust.jcommander.Parameters;
 import java.io.BufferedReader;
 import java.io.DataInputStream;
 import java.io.DataOutputStream;
@@ -56,6 +52,12 @@ import org.apache.zookeeper.ZooDefs;
 import org.apache.zookeeper.ZooKeeper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Option;
+import picocli.CommandLine.ParameterException;
+import picocli.CommandLine.Parameters;
+import picocli.CommandLine.ScopeType;
 
 /**
  * This class provides a shell for the user to dictate how simulation clients 
should incur load.
@@ -82,50 +84,50 @@ public class LoadSimulationController {
 
     private static final ExecutorService threadPool = 
Executors.newCachedThreadPool();
 
-    // JCommander arguments for starting a controller via main.
-    @Parameters(commandDescription = "Provides a shell for the user to dictate 
how simulation clients should "
-            + "incur load.")
+    // picocli arguments for starting a controller via main.
+    @Command(description = "Provides a shell for the user to dictate how 
simulation clients should "
+            + "incur load.", showDefaultValues = true, scope = 
ScopeType.INHERIT)
     private static class MainArguments {
-        @Parameter(names = { "-h", "--help" }, description = "Help message", 
help = true)
+        @Option(names = { "-h", "--help" }, description = "Help message", help 
= true)
         boolean help;
 
-        @Parameter(names = { "--cluster" }, description = "Cluster to test 
on", required = true)
+        @Option(names = { "--cluster" }, description = "Cluster to test on", 
required = true)
         String cluster;
 
-        @Parameter(names = { "--clients" }, description = "Comma separated 
list of client hostnames", required = true)
+        @Option(names = { "--clients" }, description = "Comma separated list 
of client hostnames", required = true)
         String clientHostNames;
 
-        @Parameter(names = { "--client-port" }, description = "Port that the 
clients are listening on", required = true)
+        @Option(names = { "--client-port" }, description = "Port that the 
clients are listening on", required = true)
         int clientPort;
     }
 
-    // JCommander arguments for accepting user input.
+    // picocli arguments for accepting user input.
     private static class ShellArguments {
-        @Parameter(description = "Command arguments:\n" + "trade tenant 
namespace topic\n"
+        @Parameters(description = "Command arguments:\n" + "trade tenant 
namespace topic\n"
                 + "change tenant namespace topic\n" + "stop tenant namespace 
topic\n"
                 + "trade_group tenant group_name num_namespaces\n" + 
"change_group tenant group_name\n"
                 + "stop_group tenant group_name\n" + "script script_name\n" + 
"copy tenant_name source_zk target_zk\n"
-                + "stream source_zk\n" + "simulate zk\n", required = true)
+                + "stream source_zk\n" + "simulate zk\n", arity = "1")
         List<String> commandArguments;
 
-        @Parameter(names = { "--rand-rate" }, description = "Choose message 
rate uniformly randomly from the next two "
+        @Option(names = { "--rand-rate" }, description = "Choose message rate 
uniformly randomly from the next two "
                 + "comma separated values (overrides --rate)")
         String rangeString = "";
 
-        @Parameter(names = { "--rate" }, description = "Messages per second")
+        @Option(names = { "--rate" }, description = "Messages per second")
         double rate = 1;
 
-        @Parameter(names = { "--rate-multiplier" }, description = "Multiplier 
to use for copying or streaming rates")
+        @Option(names = { "--rate-multiplier" }, description = "Multiplier to 
use for copying or streaming rates")
         double rateMultiplier = 1;
 
-        @Parameter(names = { "--separation" }, description = "Separation time 
in ms for trade_group actions "
+        @Option(names = { "--separation" }, description = "Separation time in 
ms for trade_group actions "
                 + "(0 for no separation)")
         int separation = 0;
 
-        @Parameter(names = { "--size" }, description = "Message size in bytes")
+        @Option(names = { "--size" }, description = "Message size in bytes")
         int size = 1024;
 
-        @Parameter(names = { "--topics-per-namespace" }, description = "Number 
of topics to create per namespace in "
+        @Option(names = { "--topics-per-namespace" }, description = "Number of 
topics to create per namespace in "
                 + "trade_group (total number of topics is num_namespaces X 
num_topics)")
         int topicsPerNamespace = 1;
     }
@@ -212,7 +214,7 @@ public class LoadSimulationController {
     }
 
     /**
-     * Create a LoadSimulationController with the given JCommander arguments.
+     * Create a LoadSimulationController with the given picocli arguments.
      *
      * @param arguments
      *            Arguments to create from.
@@ -318,7 +320,7 @@ public class LoadSimulationController {
         outputStream.writeDouble(arguments.rate);
     }
 
-    // Change producer settings for a given topic and JCommander arguments.
+    // Change producer settings for a given topic and picocli arguments.
     private void change(final ShellArguments arguments, final String topic, 
final int client) throws Exception {
         outputStreams[client].write(LoadSimulationClient.CHANGE_COMMAND);
         writeProducerOptions(outputStreams[client], arguments, topic);
@@ -360,7 +362,7 @@ public class LoadSimulationController {
         return clientWithTopic;
     }
 
-    // Trade using the arguments parsed via JCommander and the topic name.
+    // Trade using the arguments parsed via picocli and the topic name.
     private synchronized void trade(final ShellArguments arguments, final 
String topic, final int client)
             throws Exception {
         // Decide which client to send to randomly to preserve statelessness of
@@ -632,9 +634,9 @@ public class LoadSimulationController {
         // Don't attempt to process blank input.
         if (args.length > 0 && !(args.length == 1 && args[0].isEmpty())) {
             final ShellArguments arguments = new ShellArguments();
-            final JCommander jc = new JCommander(arguments);
+            final CommandLine commander = new CommandLine(arguments);
             try {
-                jc.parse(args);
+                commander.parseArgs(args);
                 final String command = arguments.commandArguments.get(0);
                 switch (command) {
                 case "trade":
@@ -687,8 +689,8 @@ public class LoadSimulationController {
                     log.info("ERROR: Unknown command \"{}\"", command);
                 }
             } catch (ParameterException ex) {
-                ex.printStackTrace();
-                jc.usage();
+                System.out.println(ex.getMessage());
+                commander.usage(commander.getOut());
             } catch (Exception ex) {
                 ex.printStackTrace();
             }
@@ -716,13 +718,13 @@ public class LoadSimulationController {
      */
     public static void main(String[] args) throws Exception {
         final MainArguments arguments = new MainArguments();
-        final JCommander jc = new JCommander(arguments);
-        jc.setProgramName("pulsar-perf simulation-controller");
+        final CommandLine commander = new CommandLine(arguments);
+        commander.setCommandName("pulsar-perf simulation-controller");
         try {
-            jc.parse(args);
-        } catch (Exception ex) {
-            System.out.println(ex.getMessage());
-            jc.usage();
+            commander.parseArgs(args);
+        } catch (ParameterException e) {
+            System.out.println(e.getMessage());
+            commander.usage(commander.getOut());
             PerfClientUtils.exit(1);
         }
         (new LoadSimulationController(arguments)).run();
diff --git 
a/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/ManagedLedgerWriter.java
 
b/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/ManagedLedgerWriter.java
index 336461e7a68..bad8e56a638 100644
--- 
a/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/ManagedLedgerWriter.java
+++ 
b/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/ManagedLedgerWriter.java
@@ -19,10 +19,6 @@
 package org.apache.pulsar.testclient;
 
 import static java.util.concurrent.TimeUnit.NANOSECONDS;
-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.ObjectWriter;
 import com.google.common.util.concurrent.RateLimiter;
@@ -64,6 +60,11 @@ import 
org.apache.pulsar.metadata.api.extended.MetadataStoreExtended;
 import org.apache.pulsar.testclient.utils.PaddingDecimalFormat;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Option;
+import picocli.CommandLine.ParameterException;
+import picocli.CommandLine.ScopeType;
 
 public class ManagedLedgerWriter {
 
@@ -78,61 +79,61 @@ public class ManagedLedgerWriter {
     private static Recorder recorder = new 
Recorder(TimeUnit.SECONDS.toMillis(120000), 5);
     private static Recorder cumulativeRecorder = new 
Recorder(TimeUnit.SECONDS.toMillis(120000), 5);
 
-    @Parameters(commandDescription = "Write directly on managed-ledgers")
+    @Command(description = "Write directly on managed-ledgers", 
showDefaultValues = true, scope = ScopeType.INHERIT)
     static class Arguments {
 
-        @Parameter(names = { "-h", "--help" }, description = "Help message", 
help = true)
+        @Option(names = { "-h", "--help" }, description = "Help message", help 
= true)
         boolean help;
 
-        @Parameter(names = { "-r", "--rate" }, description = "Write rate msg/s 
across managed ledgers")
+        @Option(names = { "-r", "--rate" }, description = "Write rate msg/s 
across managed ledgers")
         public int msgRate = 100;
 
-        @Parameter(names = { "-s", "--size" }, description = "Message size")
+        @Option(names = { "-s", "--size" }, description = "Message size")
         public int msgSize = 1024;
 
-        @Parameter(names = { "-t", "--num-topic" },
-                description = "Number of managed ledgers", validateWith = 
PositiveNumberParameterValidator.class)
+        @Option(names = { "-t", "--num-topic" },
+                description = "Number of managed ledgers", converter = 
PositiveNumberParameterConvert.class)
         public int numManagedLedgers = 1;
 
-        @Parameter(names = { "--threads" },
-                description = "Number of threads writing", validateWith = 
PositiveNumberParameterValidator.class)
+        @Option(names = { "--threads" },
+                description = "Number of threads writing", converter = 
PositiveNumberParameterConvert.class)
         public int numThreads = 1;
 
         @Deprecated
-        @Parameter(names = {"-zk", "--zookeeperServers"},
+        @Option(names = {"-zk", "--zookeeperServers"},
                 description = "ZooKeeper connection string",
                 hidden = true)
         public String zookeeperServers;
 
-        @Parameter(names = {"-md",
+        @Option(names = {"-md",
                 "--metadata-store"}, description = "Metadata store service 
URL. For example: zk:my-zk:2181")
         private String metadataStoreUrl;
 
-        @Parameter(names = { "-o", "--max-outstanding" }, description = "Max 
number of outstanding requests")
+        @Option(names = { "-o", "--max-outstanding" }, description = "Max 
number of outstanding requests")
         public int maxOutstanding = 1000;
 
-        @Parameter(names = { "-c",
+        @Option(names = { "-c",
                 "--max-connections" }, description = "Max number of TCP 
connections to a single bookie")
         public int maxConnections = 1;
 
-        @Parameter(names = { "-m",
+        @Option(names = { "-m",
                 "--num-messages" },
                 description = "Number of messages to publish in total. If <= 
0, it will keep publishing")
         public long numMessages = 0;
 
-        @Parameter(names = { "-e", "--ensemble-size" }, description = "Ledger 
ensemble size")
+        @Option(names = { "-e", "--ensemble-size" }, description = "Ledger 
ensemble size")
         public int ensembleSize = 1;
 
-        @Parameter(names = { "-w", "--write-quorum" }, description = "Ledger 
write quorum")
+        @Option(names = { "-w", "--write-quorum" }, description = "Ledger 
write quorum")
         public int writeQuorum = 1;
 
-        @Parameter(names = { "-a", "--ack-quorum" }, description = "Ledger ack 
quorum")
+        @Option(names = { "-a", "--ack-quorum" }, description = "Ledger ack 
quorum")
         public int ackQuorum = 1;
 
-        @Parameter(names = { "-dt", "--digest-type" }, description = 
"BookKeeper digest type")
+        @Option(names = { "-dt", "--digest-type" }, description = "BookKeeper 
digest type")
         public DigestType digestType = DigestType.CRC32C;
 
-        @Parameter(names = { "-time",
+        @Option(names = { "-time",
                 "--test-duration" }, description = "Test duration in secs. If 
<= 0, it will keep publishing")
         public long testTime = 0;
 
@@ -141,25 +142,25 @@ public class ManagedLedgerWriter {
     public static void main(String[] args) throws Exception {
 
         final Arguments arguments = new Arguments();
-        JCommander jc = new JCommander(arguments);
-        jc.setProgramName("pulsar-perf managed-ledger");
+        CommandLine commander = new CommandLine(arguments);
+        commander.setCommandName("pulsar-perf managed-ledger");
 
         try {
-            jc.parse(args);
+            commander.parseArgs(args);
         } catch (ParameterException e) {
             System.out.println(e.getMessage());
-            jc.usage();
+            commander.usage(commander.getOut());
             PerfClientUtils.exit(1);
         }
 
         if (arguments.help) {
-            jc.usage();
+            commander.usage(commander.getOut());
             PerfClientUtils.exit(1);
         }
 
         if (arguments.metadataStoreUrl == null && arguments.zookeeperServers 
== null) {
             System.err.println("Metadata store address argument is required 
(--metadata-store)");
-            jc.usage();
+            commander.usage(commander.getOut());
             PerfClientUtils.exit(1);
         }
 
diff --git 
a/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/PerformanceBaseArguments.java
 
b/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/PerformanceBaseArguments.java
index bc4ab003c46..d320cafc1a0 100644
--- 
a/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/PerformanceBaseArguments.java
+++ 
b/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/PerformanceBaseArguments.java
@@ -20,16 +20,16 @@ package org.apache.pulsar.testclient;
 
 import static org.apache.commons.lang3.StringUtils.isBlank;
 import static org.apache.pulsar.testclient.PerfClientUtils.exit;
-import com.beust.jcommander.JCommander;
-import com.beust.jcommander.Parameter;
-import com.beust.jcommander.ParameterException;
 import java.io.File;
 import java.io.FileInputStream;
 import java.util.Properties;
 import lombok.SneakyThrows;
 import org.apache.commons.lang3.StringUtils;
-import org.apache.pulsar.cli.converters.ByteUnitToLongConverter;
+import org.apache.pulsar.cli.converters.picocli.ByteUnitToLongConverter;
 import org.apache.pulsar.client.api.ProxyProtocol;
+import picocli.CommandLine;
+import picocli.CommandLine.Option;
+import picocli.CommandLine.ParameterException;
 
 /**
  * PerformanceBaseArguments contains common CLI arguments and parsing logic 
available to all sub-commands.
@@ -37,74 +37,74 @@ import org.apache.pulsar.client.api.ProxyProtocol;
  */
 public abstract class PerformanceBaseArguments {
 
-    @Parameter(names = { "-h", "--help" }, description = "Print help message", 
help = true)
+    @Option(names = { "-h", "--help" }, description = "Print help message", 
help = true)
     boolean help;
 
-    @Parameter(names = { "-cf", "--conf-file" }, description = "Pulsar 
configuration file")
+    @Option(names = { "-cf", "--conf-file" }, description = "Pulsar 
configuration file")
     public String confFile;
 
-    @Parameter(names = { "-u", "--service-url" }, description = "Pulsar 
Service URL")
+    @Option(names = { "-u", "--service-url" }, description = "Pulsar Service 
URL")
     public String serviceURL;
 
-    @Parameter(names = { "--auth-plugin" }, description = "Authentication 
plugin class name")
+    @Option(names = { "--auth-plugin" }, description = "Authentication plugin 
class name")
     public String authPluginClassName;
 
-    @Parameter(
+    @Option(
             names = { "--auth-params" },
             description = "Authentication parameters, whose format is 
determined by the implementation "
                     + "of method `configure` in authentication plugin class, 
for example \"key1:val1,key2:val2\" "
                     + "or \"{\"key1\":\"val1\",\"key2\":\"val2\"}\".")
     public String authParams;
 
-    @Parameter(names = {
+    @Option(names = {
             "--trust-cert-file" }, description = "Path for the trusted TLS 
certificate file")
     public String tlsTrustCertsFilePath = "";
 
-    @Parameter(names = {
+    @Option(names = {
             "--tls-allow-insecure" }, description = "Allow insecure TLS 
connection")
     public Boolean tlsAllowInsecureConnection = null;
 
-    @Parameter(names = {
+    @Option(names = {
             "--tls-enable-hostname-verification" }, description = "Enable TLS 
hostname verification")
     public Boolean tlsHostnameVerificationEnable = null;
 
-    @Parameter(names = { "-c",
+    @Option(names = { "-c",
             "--max-connections" }, description = "Max number of TCP 
connections to a single broker")
     public int maxConnections = 1;
 
-    @Parameter(names = { "-i",
+    @Option(names = { "-i",
             "--stats-interval-seconds" },
             description = "Statistics Interval Seconds. If 0, statistics will 
be disabled")
     public long statsIntervalSeconds = 0;
 
-    @Parameter(names = {"-ioThreads", "--num-io-threads"}, description = "Set 
the number of threads to be "
+    @Option(names = {"-ioThreads", "--num-io-threads"}, description = "Set the 
number of threads to be "
             + "used for handling connections to brokers. The default value is 
1.")
     public int ioThreads = 1;
 
-    @Parameter(names = {"-bw", "--busy-wait"}, description = "Enable Busy-Wait 
on the Pulsar client")
+    @Option(names = {"-bw", "--busy-wait"}, description = "Enable Busy-Wait on 
the Pulsar client")
     public boolean enableBusyWait = false;
 
-    @Parameter(names = { "--listener-name" }, description = "Listener name for 
the broker.")
+    @Option(names = { "--listener-name" }, description = "Listener name for 
the broker.")
     public String listenerName = null;
 
-    @Parameter(names = {"-lt", "--num-listener-threads"}, description = "Set 
the number of threads"
+    @Option(names = {"-lt", "--num-listener-threads"}, description = "Set the 
number of threads"
             + " to be used for message listeners")
     public int listenerThreads = 1;
 
-    @Parameter(names = {"-mlr", "--max-lookup-request"}, description = 
"Maximum number of lookup requests allowed "
+    @Option(names = {"-mlr", "--max-lookup-request"}, description = "Maximum 
number of lookup requests allowed "
             + "on each broker connection to prevent overloading a broker")
     public int maxLookupRequest = 50000;
 
-    @Parameter(names = { "--proxy-url" }, description = "Proxy-server URL to 
which to connect.")
+    @Option(names = { "--proxy-url" }, description = "Proxy-server URL to 
which to connect.")
     String proxyServiceURL = null;
 
-    @Parameter(names = { "--proxy-protocol" }, description = "Proxy protocol 
to select type of routing at proxy.")
+    @Option(names = { "--proxy-protocol" }, description = "Proxy protocol to 
select type of routing at proxy.")
     ProxyProtocol proxyProtocol = null;
 
-    @Parameter(names = { "--auth_plugin" }, description = "Authentication 
plugin class name", hidden = true)
+    @Option(names = { "--auth_plugin" }, description = "Authentication plugin 
class name", hidden = true)
     public String deprecatedAuthPluginClassName;
 
-    @Parameter(names = { "-ml", "--memory-limit", }, description = "Configure 
the Pulsar client memory limit "
+    @Option(names = { "-ml", "--memory-limit", }, description = "Configure the 
Pulsar client memory limit "
             + "(eg: 32M, 64M)", converter = ByteUnitToLongConverter.class)
     public long memoryLimit;
 
@@ -203,19 +203,18 @@ public abstract class PerformanceBaseArguments {
      * @throws ParameterException If there is a problem parsing the arguments
      */
     public void parseCLI(String cmdName, String[] args) {
-        JCommander jc = new JCommander(this);
-        jc.setProgramName(cmdName);
-
+        CommandLine commander = new CommandLine(this);
+        commander.setCommandName(cmdName);
         try {
-            jc.parse(args);
+            commander.parseArgs(args);
         } catch (ParameterException e) {
-            System.out.println("error: " + e.getMessage());
-            jc.usage();
+            System.out.println(e.getMessage());
+            commander.usage(commander.getOut());
             PerfClientUtils.exit(1);
         }
 
         if (help) {
-            jc.usage();
+            commander.usage(commander.getOut());
             PerfClientUtils.exit(0);
         }
 
diff --git 
a/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/PerformanceConsumer.java
 
b/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/PerformanceConsumer.java
index 7863bc49a15..7a2bc4382fd 100644
--- 
a/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/PerformanceConsumer.java
+++ 
b/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/PerformanceConsumer.java
@@ -19,8 +19,6 @@
 package org.apache.pulsar.testclient;
 
 import static org.apache.commons.lang3.StringUtils.isNotBlank;
-import com.beust.jcommander.Parameter;
-import com.beust.jcommander.Parameters;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.ObjectWriter;
 import com.google.common.util.concurrent.RateLimiter;
@@ -57,6 +55,9 @@ import org.apache.pulsar.common.naming.TopicName;
 import org.apache.pulsar.testclient.utils.PaddingDecimalFormat;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Option;
+import picocli.CommandLine.ScopeType;
 
 public class PerformanceConsumer {
     private static final LongAdder messagesReceived = new LongAdder();
@@ -82,105 +83,107 @@ public class PerformanceConsumer {
     private static final Recorder recorder = new Recorder(MAX_LATENCY, 5);
     private static final Recorder cumulativeRecorder = new 
Recorder(MAX_LATENCY, 5);
 
-    @Parameters(commandDescription = "Test pulsar consumer performance.")
+    @Command(description = "Test pulsar consumer performance.", 
showDefaultValues = true, scope = ScopeType.INHERIT)
     static class Arguments extends PerformanceTopicListArguments {
 
-        @Parameter(names = { "-n", "--num-consumers" }, description = "Number 
of consumers (per subscription), only "
+        @Option(names = { "-n", "--num-consumers" }, description = "Number of 
consumers (per subscription), only "
                 + "one consumer is allowed when subscriptionType is Exclusive",
-                validateWith = PositiveNumberParameterValidator.class)
+                converter = PositiveNumberParameterConvert.class
+        )
         public int numConsumers = 1;
 
-        @Parameter(names = { "-ns", "--num-subscriptions" }, description = 
"Number of subscriptions (per topic)",
-                validateWith = PositiveNumberParameterValidator.class)
+        @Option(names = { "-ns", "--num-subscriptions" }, description = 
"Number of subscriptions (per topic)",
+                converter = PositiveNumberParameterConvert.class
+        )
         public int numSubscriptions = 1;
 
-        @Parameter(names = { "-s", "--subscriber-name" }, description = 
"Subscriber name prefix", hidden = true)
+        @Option(names = { "-s", "--subscriber-name" }, description = 
"Subscriber name prefix", hidden = true)
         public String subscriberName;
 
-        @Parameter(names = { "-ss", "--subscriptions" },
+        @Option(names = { "-ss", "--subscriptions" },
                 description = "A list of subscriptions to consume (for 
example, sub1,sub2)")
         public List<String> subscriptions = Collections.singletonList("sub");
 
-        @Parameter(names = { "-st", "--subscription-type" }, description = 
"Subscription type")
+        @Option(names = { "-st", "--subscription-type" }, description = 
"Subscription type")
         public SubscriptionType subscriptionType = SubscriptionType.Exclusive;
 
-        @Parameter(names = { "-sp", "--subscription-position" }, description = 
"Subscription position")
+        @Option(names = { "-sp", "--subscription-position" }, description = 
"Subscription position")
         private SubscriptionInitialPosition subscriptionInitialPosition = 
SubscriptionInitialPosition.Latest;
 
-        @Parameter(names = { "-r", "--rate" }, description = "Simulate a slow 
message consumer (rate in msg/s)")
+        @Option(names = { "-r", "--rate" }, description = "Simulate a slow 
message consumer (rate in msg/s)")
         public double rate = 0;
 
-        @Parameter(names = { "-q", "--receiver-queue-size" }, description = 
"Size of the receiver queue")
+        @Option(names = { "-q", "--receiver-queue-size" }, description = "Size 
of the receiver queue")
         public int receiverQueueSize = 1000;
 
-        @Parameter(names = { "-p", "--receiver-queue-size-across-partitions" },
+        @Option(names = { "-p", "--receiver-queue-size-across-partitions" },
                 description = "Max total size of the receiver queue across 
partitions")
         public int maxTotalReceiverQueueSizeAcrossPartitions = 50000;
 
-        @Parameter(names = {"-aq", "--auto-scaled-receiver-queue-size"},
+        @Option(names = {"-aq", "--auto-scaled-receiver-queue-size"},
                 description = "Enable autoScaledReceiverQueueSize")
         public boolean autoScaledReceiverQueueSize = false;
 
-        @Parameter(names = {"-rs", "--replicated" },
+        @Option(names = {"-rs", "--replicated" },
                 description = "Whether the subscription status should be 
replicated")
         public boolean replicatedSubscription = false;
 
-        @Parameter(names = { "--acks-delay-millis" }, description = 
"Acknowledgements grouping delay in millis")
+        @Option(names = { "--acks-delay-millis" }, description = 
"Acknowledgements grouping delay in millis")
         public int acknowledgmentsGroupingDelayMillis = 100;
 
-        @Parameter(names = {"-m",
+        @Option(names = {"-m",
                 "--num-messages"},
                 description = "Number of messages to consume in total. If <= 
0, it will keep consuming")
         public long numMessages = 0;
 
-        @Parameter(names = { "-mc", "--max_chunked_msg" }, description = "Max 
pending chunk messages")
+        @Option(names = { "-mc", "--max_chunked_msg" }, description = "Max 
pending chunk messages")
         private int maxPendingChunkedMessage = 0;
 
-        @Parameter(names = { "-ac",
+        @Option(names = { "-ac",
                 "--auto_ack_chunk_q_full" }, description = "Auto ack for 
oldest message on queue is full")
         private boolean autoAckOldestChunkedMessageOnQueueFull = false;
 
-        @Parameter(names = { "-e",
+        @Option(names = { "-e",
                 "--expire_time_incomplete_chunked_messages" },
                 description = "Expire time in ms for incomplete chunk 
messages")
         private long expireTimeOfIncompleteChunkedMessageMs = 0;
 
-        @Parameter(names = { "-v",
+        @Option(names = { "-v",
                 "--encryption-key-value-file" },
                 description = "The file which contains the private key to 
decrypt payload")
         public String encKeyFile = null;
 
-        @Parameter(names = { "-time",
+        @Option(names = { "-time",
                 "--test-duration" }, description = "Test duration in secs. If 
<= 0, it will keep consuming")
         public long testTime = 0;
 
-        @Parameter(names = {"--batch-index-ack" }, description = "Enable or 
disable the batch index acknowledgment")
+        @Option(names = {"--batch-index-ack" }, description = "Enable or 
disable the batch index acknowledgment")
         public boolean batchIndexAck = false;
 
-        @Parameter(names = { "-pm", "--pool-messages" }, description = "Use 
the pooled message", arity = 1)
+        @Option(names = { "-pm", "--pool-messages" }, description = "Use the 
pooled message", arity = "1")
         private boolean poolMessages = true;
 
-        @Parameter(names = {"-tto", "--txn-timeout"},  description = "Set the 
time value of transaction timeout,"
+        @Option(names = {"-tto", "--txn-timeout"},  description = "Set the 
time value of transaction timeout,"
                 + " and the time unit is second. (After --txn-enable setting 
to true, --txn-timeout takes effect)")
         public long transactionTimeout = 10;
 
-        @Parameter(names = {"-nmt", "--numMessage-perTransaction"},
+        @Option(names = {"-nmt", "--numMessage-perTransaction"},
                 description = "The number of messages acknowledged by a 
transaction. "
                 + "(After --txn-enable setting to true, 
-numMessage-perTransaction takes effect")
         public int numMessagesPerTransaction = 50;
 
-        @Parameter(names = {"-txn", "--txn-enable"}, description = "Enable or 
disable the transaction")
+        @Option(names = {"-txn", "--txn-enable"}, description = "Enable or 
disable the transaction")
         public boolean isEnableTransaction = false;
 
-        @Parameter(names = {"-ntxn"}, description = "The number of opened 
transactions, 0 means keeping open."
+        @Option(names = {"-ntxn"}, description = "The number of opened 
transactions, 0 means keeping open."
                 + "(After --txn-enable setting to true, -ntxn takes effect.)")
         public long totalNumTxn = 0;
 
-        @Parameter(names = {"-abort"}, description = "Abort the transaction. 
(After --txn-enable "
+        @Option(names = {"-abort"}, description = "Abort the transaction. 
(After --txn-enable "
                 + "setting to true, -abort takes effect)")
         public boolean isAbortTransaction = false;
 
-        @Parameter(names = { "--histogram-file" }, description = "HdrHistogram 
output file")
+        @Option(names = { "--histogram-file" }, description = "HdrHistogram 
output file")
         public String histogramFile = null;
 
         @Override
diff --git 
a/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/PerformanceProducer.java
 
b/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/PerformanceProducer.java
index ac34bbd9f7a..0eb8d02f31e 100644
--- 
a/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/PerformanceProducer.java
+++ 
b/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/PerformanceProducer.java
@@ -25,9 +25,6 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
 import static 
org.apache.pulsar.client.impl.conf.ProducerConfigurationData.DEFAULT_BATCHING_MAX_MESSAGES;
 import static 
org.apache.pulsar.client.impl.conf.ProducerConfigurationData.DEFAULT_MAX_PENDING_MESSAGES;
 import static 
org.apache.pulsar.client.impl.conf.ProducerConfigurationData.DEFAULT_MAX_PENDING_MESSAGES_ACROSS_PARTITIONS;
-import com.beust.jcommander.IStringConverter;
-import com.beust.jcommander.Parameter;
-import com.beust.jcommander.Parameters;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.ObjectWriter;
 import com.google.common.collect.Range;
@@ -75,6 +72,11 @@ import 
org.apache.pulsar.common.partition.PartitionedTopicMetadata;
 import org.apache.pulsar.testclient.utils.PaddingDecimalFormat;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.ITypeConverter;
+import picocli.CommandLine.Option;
+import picocli.CommandLine.ScopeType;
+import picocli.CommandLine.TypeConversionException;
 
 /**
  * A client program to test pulsar producer performance.
@@ -103,151 +105,153 @@ public class PerformanceProducer {
 
     private static IMessageFormatter messageFormatter = null;
 
-    @Parameters(commandDescription = "Test pulsar producer performance.")
+    @Command(description = "Test pulsar producer performance.", 
showDefaultValues = true, scope = ScopeType.INHERIT)
     static class Arguments extends PerformanceTopicListArguments {
 
-        @Parameter(names = { "-threads", "--num-test-threads" }, description = 
"Number of test threads",
-                validateWith = PositiveNumberParameterValidator.class)
+        @Option(names = { "-threads", "--num-test-threads" }, description = 
"Number of test threads",
+                converter = PositiveNumberParameterConvert.class
+        )
         public int numTestThreads = 1;
 
-        @Parameter(names = { "-r", "--rate" }, description = "Publish rate 
msg/s across topics")
+        @Option(names = { "-r", "--rate" }, description = "Publish rate msg/s 
across topics")
         public int msgRate = 100;
 
-        @Parameter(names = { "-s", "--size" }, description = "Message size 
(bytes)")
+        @Option(names = { "-s", "--size" }, description = "Message size 
(bytes)")
         public int msgSize = 1024;
 
-        @Parameter(names = { "-n", "--num-producers" }, description = "Number 
of producers (per topic)",
-                validateWith = PositiveNumberParameterValidator.class)
+        @Option(names = { "-n", "--num-producers" }, description = "Number of 
producers (per topic)",
+                converter = PositiveNumberParameterConvert.class
+        )
         public int numProducers = 1;
 
-        @Parameter(names = {"--separator"}, description = "Separator between 
the topic and topic number")
+        @Option(names = {"--separator"}, description = "Separator between the 
topic and topic number")
         public String separator = "-";
 
-        @Parameter(names = {"--send-timeout"}, description = "Set the 
sendTimeout value default 0 to keep "
+        @Option(names = {"--send-timeout"}, description = "Set the sendTimeout 
value default 0 to keep "
                 + "compatibility with previous version of pulsar-perf")
         public int sendTimeout = 0;
 
-        @Parameter(names = { "-pn", "--producer-name" }, description = 
"Producer Name")
+        @Option(names = { "-pn", "--producer-name" }, description = "Producer 
Name")
         public String producerName = null;
 
-        @Parameter(names = { "-au", "--admin-url" }, description = "Pulsar 
Admin URL")
+        @Option(names = { "-au", "--admin-url" }, description = "Pulsar Admin 
URL")
         public String adminURL;
 
-        @Parameter(names = { "-ch",
+        @Option(names = { "-ch",
                 "--chunking" }, description = "Should split the message and 
publish in chunks if message size is "
                 + "larger than allowed max size")
         private boolean chunkingAllowed = false;
 
-        @Parameter(names = { "-o", "--max-outstanding" }, description = "Max 
number of outstanding messages")
+        @Option(names = { "-o", "--max-outstanding" }, description = "Max 
number of outstanding messages")
         public int maxOutstanding = DEFAULT_MAX_PENDING_MESSAGES;
 
-        @Parameter(names = { "-p", "--max-outstanding-across-partitions" }, 
description = "Max number of outstanding "
+        @Option(names = { "-p", "--max-outstanding-across-partitions" }, 
description = "Max number of outstanding "
                 + "messages across partitions")
         public int maxPendingMessagesAcrossPartitions = 
DEFAULT_MAX_PENDING_MESSAGES_ACROSS_PARTITIONS;
 
-        @Parameter(names = { "-np", "--partitions" }, description = "Create 
partitioned topics with the given number "
+        @Option(names = { "-np", "--partitions" }, description = "Create 
partitioned topics with the given number "
                 + "of partitions, set 0 to not try to create the topic")
         public Integer partitions = null;
 
-        @Parameter(names = { "-m",
+        @Option(names = { "-m",
                 "--num-messages" }, description = "Number of messages to 
publish in total. If <= 0, it will keep "
                 + "publishing")
         public long numMessages = 0;
 
-        @Parameter(names = { "-z", "--compression" }, description = "Compress 
messages payload")
+        @Option(names = { "-z", "--compression" }, description = "Compress 
messages payload")
         public CompressionType compression = CompressionType.NONE;
 
-        @Parameter(names = { "-f", "--payload-file" }, description = "Use 
payload from an UTF-8 encoded text file and "
+        @Option(names = { "-f", "--payload-file" }, description = "Use payload 
from an UTF-8 encoded text file and "
                 + "a payload will be randomly selected when publishing 
messages")
         public String payloadFilename = null;
 
-        @Parameter(names = { "-e", "--payload-delimiter" }, description = "The 
delimiter used to split lines when "
+        @Option(names = { "-e", "--payload-delimiter" }, description = "The 
delimiter used to split lines when "
                 + "using payload from a file")
         // here escaping \n since default value will be printed with the help 
text
         public String payloadDelimiter = "\\n";
 
-        @Parameter(names = { "-b",
+        @Option(names = { "-b",
                 "--batch-time-window" }, description = "Batch messages in 'x' 
ms window (Default: 1ms)")
         public double batchTimeMillis = 1.0;
 
-        @Parameter(names = { "-db",
+        @Option(names = { "-db",
                 "--disable-batching" }, description = "Disable batching if 
true")
         public boolean disableBatching;
 
-        @Parameter(names = {
+        @Option(names = {
             "-bm", "--batch-max-messages"
         }, description = "Maximum number of messages per batch")
         public int batchMaxMessages = DEFAULT_BATCHING_MAX_MESSAGES;
 
-        @Parameter(names = {
+        @Option(names = {
             "-bb", "--batch-max-bytes"
         }, description = "Maximum number of bytes per batch")
         public int batchMaxBytes = 4 * 1024 * 1024;
 
-        @Parameter(names = { "-time",
+        @Option(names = { "-time",
                 "--test-duration" }, description = "Test duration in secs. If 
<= 0, it will keep publishing")
         public long testTime = 0;
 
-        @Parameter(names = "--warmup-time", description = "Warm-up time in 
seconds (Default: 1 sec)")
+        @Option(names = "--warmup-time", description = "Warm-up time in 
seconds (Default: 1 sec)")
         public double warmupTimeSeconds = 1.0;
 
-        @Parameter(names = { "-k", "--encryption-key-name" }, description = 
"The public key name to encrypt payload")
+        @Option(names = { "-k", "--encryption-key-name" }, description = "The 
public key name to encrypt payload")
         public String encKeyName = null;
 
-        @Parameter(names = { "-v",
+        @Option(names = { "-v",
                 "--encryption-key-value-file" },
                 description = "The file which contains the public key to 
encrypt payload")
         public String encKeyFile = null;
 
-        @Parameter(names = { "-d",
+        @Option(names = { "-d",
                 "--delay" }, description = "Mark messages with a given delay 
in seconds")
         public long delay = 0;
 
-        @Parameter(names = { "-dr", "--delay-range"}, description = "Mark 
messages with a given delay by a random"
+        @Option(names = { "-dr", "--delay-range"}, description = "Mark 
messages with a given delay by a random"
                 + " number of seconds. this value between the specified origin 
(inclusive) and the specified bound"
                 + " (exclusive). e.g. 1,300", converter = RangeConvert.class)
         public Range<Long> delayRange = null;
 
-        @Parameter(names = { "-set",
+        @Option(names = { "-set",
                 "--set-event-time" }, description = "Set the eventTime on 
messages")
         public boolean setEventTime = false;
 
-        @Parameter(names = { "-ef",
+        @Option(names = { "-ef",
                 "--exit-on-failure" }, description = "Exit from the process on 
publish failure (default: disable)")
         public boolean exitOnFailure = false;
 
-        @Parameter(names = {"-mk", "--message-key-generation-mode"}, 
description = "The generation mode of message key"
+        @Option(names = {"-mk", "--message-key-generation-mode"}, description 
= "The generation mode of message key"
                 + ", valid options are: [autoIncrement, random]")
         public String messageKeyGenerationMode = null;
 
-        @Parameter(names = { "-am", "--access-mode" }, description = "Producer 
access mode")
+        @Option(names = { "-am", "--access-mode" }, description = "Producer 
access mode")
         public ProducerAccessMode producerAccessMode = 
ProducerAccessMode.Shared;
 
-        @Parameter(names = { "-fp", "--format-payload" },
-                description = "Format %i as a message index in the stream from 
producer and/or %t as the timestamp"
+        @Option(names = { "-fp", "--format-payload" },
+                description = "Format %%i as a message index in the stream 
from producer and/or %%t as the timestamp"
                         + " nanoseconds.")
         public boolean formatPayload = false;
 
-        @Parameter(names = {"-fc", "--format-class"}, description = "Custom 
Formatter class name")
+        @Option(names = {"-fc", "--format-class"}, description = "Custom 
Formatter class name")
         public String formatterClass = 
"org.apache.pulsar.testclient.DefaultMessageFormatter";
 
-        @Parameter(names = {"-tto", "--txn-timeout"}, description = "Set the 
time value of transaction timeout,"
+        @Option(names = {"-tto", "--txn-timeout"}, description = "Set the time 
value of transaction timeout,"
                 + " and the time unit is second. (After --txn-enable setting 
to true, --txn-timeout takes effect)")
         public long transactionTimeout = 10;
 
-        @Parameter(names = {"-nmt", "--numMessage-perTransaction"},
+        @Option(names = {"-nmt", "--numMessage-perTransaction"},
                 description = "The number of messages sent by a transaction. "
                         + "(After --txn-enable setting to true, -nmt takes 
effect)")
         public int numMessagesPerTransaction = 50;
 
-        @Parameter(names = {"-txn", "--txn-enable"}, description = "Enable or 
disable the transaction")
+        @Option(names = {"-txn", "--txn-enable"}, description = "Enable or 
disable the transaction")
         public boolean isEnableTransaction = false;
 
-        @Parameter(names = {"-abort"}, description = "Abort the transaction. 
(After --txn-enable "
+        @Option(names = {"-abort"}, description = "Abort the transaction. 
(After --txn-enable "
                 + "setting to true, -abort takes effect)")
         public boolean isAbortTransaction = false;
 
-        @Parameter(names = { "--histogram-file" }, description = "HdrHistogram 
output file")
+        @Option(names = { "--histogram-file" }, description = "HdrHistogram 
output file")
         public String histogramFile = null;
 
         @Override
@@ -794,7 +798,7 @@ public class PerformanceProducer {
         autoIncrement, random
     }
 
-    static class RangeConvert implements IStringConverter<Range<Long>> {
+    static class RangeConvert implements ITypeConverter<Range<Long>> {
         @Override
         public Range<Long> convert(String rangeStr) {
             try {
@@ -804,7 +808,7 @@ public class PerformanceProducer {
                 final long max = Long.parseLong(facts[1].trim());
                 return Range.closedOpen(min, max);
             } catch (Throwable ex) {
-                throw new IllegalArgumentException("Unknown delay range 
interval,"
+                throw new TypeConversionException("Unknown delay range 
interval,"
                         + " the format should be \"<origin>,<bound>\". error 
message: " + rangeStr);
             }
         }
diff --git 
a/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/PerformanceReader.java
 
b/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/PerformanceReader.java
index ed5cc37644a..3572cbde43c 100644
--- 
a/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/PerformanceReader.java
+++ 
b/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/PerformanceReader.java
@@ -18,8 +18,6 @@
  */
 package org.apache.pulsar.testclient;
 
-import com.beust.jcommander.Parameter;
-import com.beust.jcommander.Parameters;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.ObjectWriter;
 import com.google.common.util.concurrent.RateLimiter;
@@ -46,6 +44,9 @@ import org.apache.pulsar.common.util.FutureUtil;
 import org.apache.pulsar.testclient.utils.PaddingDecimalFormat;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Option;
+import picocli.CommandLine.ScopeType;
 
 public class PerformanceReader {
     private static final LongAdder messagesReceived = new LongAdder();
@@ -59,30 +60,30 @@ public class PerformanceReader {
     private static Recorder recorder = new 
Recorder(TimeUnit.DAYS.toMillis(10), 5);
     private static Recorder cumulativeRecorder = new 
Recorder(TimeUnit.DAYS.toMillis(10), 5);
 
-    @Parameters(commandDescription = "Test pulsar reader performance.")
+    @Command(description = "Test pulsar reader performance.", 
showDefaultValues = true, scope = ScopeType.INHERIT)
     static class Arguments extends PerformanceTopicListArguments {
 
-        @Parameter(names = { "-r", "--rate" }, description = "Simulate a slow 
message reader (rate in msg/s)")
+        @Option(names = { "-r", "--rate" }, description = "Simulate a slow 
message reader (rate in msg/s)")
         public double rate = 0;
 
-        @Parameter(names = { "-m",
+        @Option(names = { "-m",
                 "--start-message-id" }, description = "Start message id. This 
can be either 'earliest', "
                 + "'latest' or a specific message id by using 'lid:eid'")
         public String startMessageId = "earliest";
 
-        @Parameter(names = { "-q", "--receiver-queue-size" }, description = 
"Size of the receiver queue")
+        @Option(names = { "-q", "--receiver-queue-size" }, description = "Size 
of the receiver queue")
         public int receiverQueueSize = 1000;
 
-        @Parameter(names = {"-n",
+        @Option(names = {"-n",
                 "--num-messages"}, description = "Number of messages to 
consume in total. If <= 0, "
                 + "it will keep consuming")
         public long numMessages = 0;
 
-        @Parameter(names = {
+        @Option(names = {
                 "--use-tls" }, description = "Use TLS encryption on the 
connection")
         public boolean useTls;
 
-        @Parameter(names = { "-time",
+        @Option(names = { "-time",
                 "--test-duration" }, description = "Test duration in secs. If 
<= 0, it will keep consuming")
         public long testTime = 0;
 
diff --git 
a/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/PerformanceTopicListArguments.java
 
b/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/PerformanceTopicListArguments.java
index a2f8b6af082..9ac99d0abcc 100644
--- 
a/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/PerformanceTopicListArguments.java
+++ 
b/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/PerformanceTopicListArguments.java
@@ -18,10 +18,11 @@
  */
 package org.apache.pulsar.testclient;
 
-import com.beust.jcommander.Parameter;
 import java.util.ArrayList;
 import java.util.List;
 import org.apache.pulsar.common.naming.TopicName;
+import picocli.CommandLine.Option;
+import picocli.CommandLine.Parameters;
 
 /**
  * PerformanceTopicListArguments provides common topic list arguments which 
are used
@@ -29,12 +30,13 @@ import org.apache.pulsar.common.naming.TopicName;
  */
 public abstract class PerformanceTopicListArguments extends 
PerformanceBaseArguments {
 
-    @Parameter(description = "persistent://prop/ns/my-topic", required = true)
+    @Parameters(description = "persistent://prop/ns/my-topic", arity = "1")
     public List<String> topics;
 
-    @Parameter(names = { "-t", "--num-topics", "--num-topic" }, description = 
"Number of topics.  Must match"
+    @Option(names = { "-t", "--num-topics", "--num-topic" }, description = 
"Number of topics.  Must match"
             + "the given number of topic arguments.",
-            validateWith = PositiveNumberParameterValidator.class)
+            converter = PositiveNumberParameterConvert.class
+    )
     public int numTopics = 1;
 
     @Override
diff --git 
a/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/PerformanceTransaction.java
 
b/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/PerformanceTransaction.java
index 0bfa216c459..02e50ab4e2b 100644
--- 
a/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/PerformanceTransaction.java
+++ 
b/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/PerformanceTransaction.java
@@ -19,8 +19,6 @@
 package org.apache.pulsar.testclient;
 
 import static java.util.concurrent.TimeUnit.NANOSECONDS;
-import com.beust.jcommander.Parameter;
-import com.beust.jcommander.Parameters;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.ObjectWriter;
 import com.google.common.util.concurrent.RateLimiter;
@@ -65,6 +63,9 @@ import 
org.apache.pulsar.common.partition.PartitionedTopicMetadata;
 import org.apache.pulsar.testclient.utils.PaddingDecimalFormat;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Option;
+import picocli.CommandLine.ScopeType;
 
 public class PerformanceTransaction {
 
@@ -89,84 +90,84 @@ public class PerformanceTransaction {
     private static final Recorder messageSendRCumulativeRecorder =
             new Recorder(TimeUnit.SECONDS.toMicros(120000), 5);
 
-    @Parameters(commandDescription = "Test pulsar transaction performance.")
+    @Command(description = "Test pulsar transaction performance.", 
showDefaultValues = true, scope = ScopeType.INHERIT)
     static class Arguments extends PerformanceBaseArguments {
 
-        @Parameter(names = "--topics-c", description = "All topics that need 
ack for a transaction", required =
+        @Option(names = "--topics-c", description = "All topics that need ack 
for a transaction", required =
                 true)
         public List<String> consumerTopic = 
Collections.singletonList("test-consume");
 
-        @Parameter(names = "--topics-p", description = "All topics that need 
produce for a transaction",
+        @Option(names = "--topics-p", description = "All topics that need 
produce for a transaction",
                 required = true)
         public List<String> producerTopic = 
Collections.singletonList("test-produce");
 
-        @Parameter(names = {"-threads", "--num-test-threads"}, description = 
"Number of test threads."
+        @Option(names = {"-threads", "--num-test-threads"}, description = 
"Number of test threads."
                 + "This thread is for a new transaction to ack messages from 
consumer topics and produce message to "
                 + "producer topics, and then commit or abort this transaction. 
"
                 + "Increasing the number of threads increases the parallelism 
of the performance test, "
                 + "thereby increasing the intensity of the stress test.")
         public int numTestThreads = 1;
 
-        @Parameter(names = {"-au", "--admin-url"}, description = "Pulsar Admin 
URL")
+        @Option(names = {"-au", "--admin-url"}, description = "Pulsar Admin 
URL")
         public String adminURL;
 
-        @Parameter(names = {"-np",
+        @Option(names = {"-np",
                 "--partitions"}, description = "Create partitioned topics with 
a given number of partitions, 0 means"
                 + "not trying to create a topic")
         public Integer partitions = null;
 
-        @Parameter(names = {"-time",
+        @Option(names = {"-time",
                 "--test-duration"}, description = "Test duration (in second). 
0 means keeping publishing")
         public long testTime = 0;
 
-        @Parameter(names = {"-ss",
+        @Option(names = {"-ss",
                 "--subscriptions"}, description = "A list of subscriptions to 
consume (for example, sub1,sub2)")
         public List<String> subscriptions = Collections.singletonList("sub");
 
-        @Parameter(names = {"-ns", "--num-subscriptions"}, description = 
"Number of subscriptions (per topic)")
+        @Option(names = {"-ns", "--num-subscriptions"}, description = "Number 
of subscriptions (per topic)")
         public int numSubscriptions = 1;
 
-        @Parameter(names = {"-sp", "--subscription-position"}, description = 
"Subscription position")
+        @Option(names = {"-sp", "--subscription-position"}, description = 
"Subscription position")
         private SubscriptionInitialPosition subscriptionInitialPosition = 
SubscriptionInitialPosition.Earliest;
 
-        @Parameter(names = {"-st", "--subscription-type"}, description = 
"Subscription type")
+        @Option(names = {"-st", "--subscription-type"}, description = 
"Subscription type")
         public SubscriptionType subscriptionType = SubscriptionType.Shared;
 
-        @Parameter(names = {"-rs", "--replicated" },
+        @Option(names = {"-rs", "--replicated" },
                 description = "Whether the subscription status should be 
replicated")
         private boolean replicatedSubscription = false;
 
-        @Parameter(names = {"-q", "--receiver-queue-size"}, description = 
"Size of the receiver queue")
+        @Option(names = {"-q", "--receiver-queue-size"}, description = "Size 
of the receiver queue")
         public int receiverQueueSize = 1000;
 
-        @Parameter(names = {"-tto", "--txn-timeout"}, description = "Set the 
time value of transaction timeout,"
+        @Option(names = {"-tto", "--txn-timeout"}, description = "Set the time 
value of transaction timeout,"
                 + " and the time unit is second. (After --txn-enable setting 
to true, --txn-timeout takes effect)")
         public long transactionTimeout = 5;
 
-        @Parameter(names = {"-ntxn",
+        @Option(names = {"-ntxn",
                 "--number-txn"}, description = "Set the number of transaction. 
0 means keeping open."
                 + "If transaction disabled, it means the number of tasks. The 
task or transaction produces or "
                 + "consumes a specified number of messages.")
         public long numTransactions = 0;
 
-        @Parameter(names = {"-nmp", "--numMessage-perTransaction-produce"},
+        @Option(names = {"-nmp", "--numMessage-perTransaction-produce"},
                 description = "Set the number of messages produced in  a 
transaction."
                         + "If transaction disabled, it means the number of 
messages produced in a task.")
         public int numMessagesProducedPerTransaction = 1;
 
-        @Parameter(names = {"-nmc", "--numMessage-perTransaction-consume"},
+        @Option(names = {"-nmc", "--numMessage-perTransaction-consume"},
                 description = "Set the number of messages consumed in a 
transaction."
                         + "If transaction disabled, it means the number of 
messages consumed in a task.")
         public int numMessagesReceivedPerTransaction = 1;
 
-        @Parameter(names = {"--txn-disable"}, description = "Disable 
transaction")
+        @Option(names = {"--txn-disable"}, description = "Disable transaction")
         public boolean isDisableTransaction = false;
 
-        @Parameter(names = {"-abort"}, description = "Abort the transaction. 
(After --txn-disEnable "
+        @Option(names = {"-abort"}, description = "Abort the transaction. 
(After --txn-disEnable "
                 + "setting to false, -abort takes effect)")
         public boolean isAbortTransaction = false;
 
-        @Parameter(names = "-txnRate", description = "Set the rate of opened 
transaction or task. 0 means no limit")
+        @Option(names = "-txnRate", description = "Set the rate of opened 
transaction or task. 0 means no limit")
         public int openTxnRate = 0;
 
         @Override
diff --git 
a/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/PositiveNumberParameterValidator.java
 
b/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/PositiveNumberParameterConvert.java
similarity index 68%
rename from 
pulsar-testclient/src/main/java/org/apache/pulsar/testclient/PositiveNumberParameterValidator.java
rename to 
pulsar-testclient/src/main/java/org/apache/pulsar/testclient/PositiveNumberParameterConvert.java
index 7e8fe2181cd..fc045eb8aaf 100644
--- 
a/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/PositiveNumberParameterValidator.java
+++ 
b/pulsar-testclient/src/main/java/org/apache/pulsar/testclient/PositiveNumberParameterConvert.java
@@ -18,15 +18,16 @@
  */
 package org.apache.pulsar.testclient;
 
-import com.beust.jcommander.IParameterValidator;
-import com.beust.jcommander.ParameterException;
-
-public class PositiveNumberParameterValidator implements IParameterValidator {
+import picocli.CommandLine.ITypeConverter;
+import picocli.CommandLine.TypeConversionException;
 
+public class PositiveNumberParameterConvert implements ITypeConverter<Integer> 
{
     @Override
-    public void validate(String name, String value) throws ParameterException {
-        if (Integer.parseInt(value) <= 0) {
-            throw new ParameterException("Parameter " + name + " should be > 0 
(found " + value + ")");
+    public Integer convert(String value) {
+        int result = Integer.parseInt(value);
+        if (result <= 0) {
+            throw new TypeConversionException("Parameter should be > 0 (found 
" + value + ")");
         }
+        return result;
     }
 }
diff --git 
a/pulsar-testclient/src/test/java/org/apache/pulsar/testclient/GenerateDocumentionTest.java
 
b/pulsar-testclient/src/test/java/org/apache/pulsar/testclient/GenerateDocumentionTest.java
index 936275bcd41..73d7751e333 100644
--- 
a/pulsar-testclient/src/test/java/org/apache/pulsar/testclient/GenerateDocumentionTest.java
+++ 
b/pulsar-testclient/src/test/java/org/apache/pulsar/testclient/GenerateDocumentionTest.java
@@ -18,7 +18,9 @@
  */
 package org.apache.pulsar.testclient;
 
+import org.testng.Assert;
 import org.testng.annotations.Test;
+import picocli.CommandLine;
 
 public class GenerateDocumentionTest {
 
@@ -32,4 +34,39 @@ public class GenerateDocumentionTest {
         String[] args = new String[]{"-n", "produce", "-n", "consume"};
         CmdGenerateDocumentation.main(args);
     }
+
+    private static final String DESC = "desc";
+    @Test
+    public void testGetCommandOptionDescription(){
+        Arguments arguments = new Arguments();
+        CommandLine commander = new CommandLine(arguments);
+        String desc = 
CmdGenerateDocumentation.getCommandDescription(commander);
+        Assert.assertEquals(desc, DESC);
+
+        commander.getCommandSpec().options().forEach(option -> {
+            String desc1 = 
CmdGenerateDocumentation.getOptionDescription(option);
+            Assert.assertEquals(desc1, DESC);
+        });
+
+        ArgumentsWithoutDesc argumentsWithoutDesc = new ArgumentsWithoutDesc();
+        commander = new CommandLine(argumentsWithoutDesc);
+        desc = CmdGenerateDocumentation.getCommandDescription(commander);
+        Assert.assertEquals(desc, "");
+
+        commander.getCommandSpec().options().forEach(option -> {
+            String desc1 = 
CmdGenerateDocumentation.getOptionDescription(option);
+            Assert.assertEquals(desc1, "");
+        });
+    }
+
+    @CommandLine.Command(description = DESC)
+    static class Arguments {
+        @CommandLine.Option(names = {"-h", "--help"}, description = DESC, help 
= true)
+        boolean help;
+    }
+    @CommandLine.Command()
+    static class ArgumentsWithoutDesc {
+        @CommandLine.Option(names = {"-h", "--help"}, help = true)
+        boolean help;
+    }
 }
\ No newline at end of file


Reply via email to