You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@bookkeeper.apache.org by GitBox <gi...@apache.org> on 2018/06/04 20:30:54 UTC

[GitHub] sijie closed pull request #1471: Abstract the tools framework to allow merging multiple CLI tools together

sijie closed pull request #1471: Abstract the tools framework to allow merging multiple CLI tools together
URL: https://github.com/apache/bookkeeper/pull/1471
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/pom.xml b/pom.xml
index ece16cc7d..43417dfbb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -57,7 +57,6 @@
     <module>bookkeeper-stats</module>
     <module>bookkeeper-proto</module>
     <module>bookkeeper-server</module>
-    <module>bookkeeper-tools</module>
     <module>bookkeeper-benchmark</module>
     <module>bookkeeper-stats-providers</module>
     <module>bookkeeper-http</module>
@@ -66,6 +65,7 @@
     <module>bookkeeper-dist</module>
     <module>microbenchmarks</module>
     <module>stream/distributedlog</module>
+    <module>tools</module>
   </modules>
   <mailingLists>
     <mailingList>
diff --git a/bookkeeper-tools/pom.xml b/tools/all/pom.xml
similarity index 90%
rename from bookkeeper-tools/pom.xml
rename to tools/all/pom.xml
index 37544d12c..db6d91dec 100644
--- a/bookkeeper-tools/pom.xml
+++ b/tools/all/pom.xml
@@ -18,7 +18,7 @@
 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <parent>
-    <artifactId>bookkeeper</artifactId>
+    <artifactId>bookkeeper-tools-parent</artifactId>
     <groupId>org.apache.bookkeeper</groupId>
     <version>4.8.0-SNAPSHOT</version>
   </parent>
@@ -27,12 +27,13 @@
   <dependencies>
     <dependency>
       <groupId>org.apache.bookkeeper</groupId>
-      <artifactId>bookkeeper-server</artifactId>
-      <version>${project.parent.version}</version>
+      <artifactId>bookkeeper-tools-framework</artifactId>
+      <version>${project.version}</version>
     </dependency>
     <dependency>
-      <groupId>com.beust</groupId>
-      <artifactId>jcommander</artifactId>
+      <groupId>org.apache.bookkeeper</groupId>
+      <artifactId>bookkeeper-server</artifactId>
+      <version>${project.parent.version}</version>
     </dependency>
     <dependency>
       <groupId>org.apache.bookkeeper</groupId>
diff --git a/bookkeeper-tools/src/main/java/org/apache/bookkeeper/tools/cli/BookKeeperCLI.java b/tools/all/src/main/java/org/apache/bookkeeper/tools/cli/BookKeeperCLI.java
similarity index 100%
rename from bookkeeper-tools/src/main/java/org/apache/bookkeeper/tools/cli/BookKeeperCLI.java
rename to tools/all/src/main/java/org/apache/bookkeeper/tools/cli/BookKeeperCLI.java
diff --git a/bookkeeper-tools/src/main/java/org/apache/bookkeeper/tools/cli/commands/CmdBase.java b/tools/all/src/main/java/org/apache/bookkeeper/tools/cli/commands/CmdBase.java
similarity index 100%
rename from bookkeeper-tools/src/main/java/org/apache/bookkeeper/tools/cli/commands/CmdBase.java
rename to tools/all/src/main/java/org/apache/bookkeeper/tools/cli/commands/CmdBase.java
diff --git a/bookkeeper-tools/src/main/java/org/apache/bookkeeper/tools/cli/commands/CmdBookie.java b/tools/all/src/main/java/org/apache/bookkeeper/tools/cli/commands/CmdBookie.java
similarity index 100%
rename from bookkeeper-tools/src/main/java/org/apache/bookkeeper/tools/cli/commands/CmdBookie.java
rename to tools/all/src/main/java/org/apache/bookkeeper/tools/cli/commands/CmdBookie.java
diff --git a/bookkeeper-tools/src/main/java/org/apache/bookkeeper/tools/cli/commands/CmdClient.java b/tools/all/src/main/java/org/apache/bookkeeper/tools/cli/commands/CmdClient.java
similarity index 100%
rename from bookkeeper-tools/src/main/java/org/apache/bookkeeper/tools/cli/commands/CmdClient.java
rename to tools/all/src/main/java/org/apache/bookkeeper/tools/cli/commands/CmdClient.java
diff --git a/bookkeeper-tools/src/main/java/org/apache/bookkeeper/tools/cli/commands/CmdCluster.java b/tools/all/src/main/java/org/apache/bookkeeper/tools/cli/commands/CmdCluster.java
similarity index 100%
rename from bookkeeper-tools/src/main/java/org/apache/bookkeeper/tools/cli/commands/CmdCluster.java
rename to tools/all/src/main/java/org/apache/bookkeeper/tools/cli/commands/CmdCluster.java
diff --git a/bookkeeper-tools/src/main/java/org/apache/bookkeeper/tools/cli/commands/CmdMetadata.java b/tools/all/src/main/java/org/apache/bookkeeper/tools/cli/commands/CmdMetadata.java
similarity index 100%
rename from bookkeeper-tools/src/main/java/org/apache/bookkeeper/tools/cli/commands/CmdMetadata.java
rename to tools/all/src/main/java/org/apache/bookkeeper/tools/cli/commands/CmdMetadata.java
diff --git a/bookkeeper-tools/src/main/java/org/apache/bookkeeper/tools/cli/commands/package-info.java b/tools/all/src/main/java/org/apache/bookkeeper/tools/cli/commands/package-info.java
similarity index 100%
rename from bookkeeper-tools/src/main/java/org/apache/bookkeeper/tools/cli/commands/package-info.java
rename to tools/all/src/main/java/org/apache/bookkeeper/tools/cli/commands/package-info.java
diff --git a/bookkeeper-tools/src/main/java/org/apache/bookkeeper/tools/cli/package-info.java b/tools/all/src/main/java/org/apache/bookkeeper/tools/cli/package-info.java
similarity index 100%
rename from bookkeeper-tools/src/main/java/org/apache/bookkeeper/tools/cli/package-info.java
rename to tools/all/src/main/java/org/apache/bookkeeper/tools/cli/package-info.java
diff --git a/bookkeeper-tools/src/test/java/org/apache/bookkeeper/tools/cli/BookKeeperCLITest.java b/tools/all/src/test/java/org/apache/bookkeeper/tools/cli/BookKeeperCLITest.java
similarity index 100%
rename from bookkeeper-tools/src/test/java/org/apache/bookkeeper/tools/cli/BookKeeperCLITest.java
rename to tools/all/src/test/java/org/apache/bookkeeper/tools/cli/BookKeeperCLITest.java
diff --git a/bookkeeper-tools/src/test/java/org/apache/bookkeeper/tools/cli/commands/CmdBaseTest.java b/tools/all/src/test/java/org/apache/bookkeeper/tools/cli/commands/CmdBaseTest.java
similarity index 100%
rename from bookkeeper-tools/src/test/java/org/apache/bookkeeper/tools/cli/commands/CmdBaseTest.java
rename to tools/all/src/test/java/org/apache/bookkeeper/tools/cli/commands/CmdBaseTest.java
diff --git a/bookkeeper-tools/src/test/java/org/apache/bookkeeper/tools/cli/commands/bookie/LastMarkCommandTest.java b/tools/all/src/test/java/org/apache/bookkeeper/tools/cli/commands/bookie/LastMarkCommandTest.java
similarity index 100%
rename from bookkeeper-tools/src/test/java/org/apache/bookkeeper/tools/cli/commands/bookie/LastMarkCommandTest.java
rename to tools/all/src/test/java/org/apache/bookkeeper/tools/cli/commands/bookie/LastMarkCommandTest.java
diff --git a/bookkeeper-tools/src/test/java/org/apache/bookkeeper/tools/cli/commands/client/SimpleTestCommandTest.java b/tools/all/src/test/java/org/apache/bookkeeper/tools/cli/commands/client/SimpleTestCommandTest.java
similarity index 100%
rename from bookkeeper-tools/src/test/java/org/apache/bookkeeper/tools/cli/commands/client/SimpleTestCommandTest.java
rename to tools/all/src/test/java/org/apache/bookkeeper/tools/cli/commands/client/SimpleTestCommandTest.java
diff --git a/bookkeeper-tools/src/test/java/org/apache/bookkeeper/tools/cli/commands/cluster/ListBookiesCommandTest.java b/tools/all/src/test/java/org/apache/bookkeeper/tools/cli/commands/cluster/ListBookiesCommandTest.java
similarity index 100%
rename from bookkeeper-tools/src/test/java/org/apache/bookkeeper/tools/cli/commands/cluster/ListBookiesCommandTest.java
rename to tools/all/src/test/java/org/apache/bookkeeper/tools/cli/commands/cluster/ListBookiesCommandTest.java
diff --git a/bookkeeper-tools/src/test/java/org/apache/bookkeeper/tools/cli/helpers/BookieCommandTestBase.java b/tools/all/src/test/java/org/apache/bookkeeper/tools/cli/helpers/BookieCommandTestBase.java
similarity index 100%
rename from bookkeeper-tools/src/test/java/org/apache/bookkeeper/tools/cli/helpers/BookieCommandTestBase.java
rename to tools/all/src/test/java/org/apache/bookkeeper/tools/cli/helpers/BookieCommandTestBase.java
diff --git a/bookkeeper-tools/src/test/java/org/apache/bookkeeper/tools/cli/helpers/ClientCommandTest.java b/tools/all/src/test/java/org/apache/bookkeeper/tools/cli/helpers/ClientCommandTest.java
similarity index 100%
rename from bookkeeper-tools/src/test/java/org/apache/bookkeeper/tools/cli/helpers/ClientCommandTest.java
rename to tools/all/src/test/java/org/apache/bookkeeper/tools/cli/helpers/ClientCommandTest.java
diff --git a/bookkeeper-tools/src/test/java/org/apache/bookkeeper/tools/cli/helpers/ClientCommandTestBase.java b/tools/all/src/test/java/org/apache/bookkeeper/tools/cli/helpers/ClientCommandTestBase.java
similarity index 100%
rename from bookkeeper-tools/src/test/java/org/apache/bookkeeper/tools/cli/helpers/ClientCommandTestBase.java
rename to tools/all/src/test/java/org/apache/bookkeeper/tools/cli/helpers/ClientCommandTestBase.java
diff --git a/bookkeeper-tools/src/test/java/org/apache/bookkeeper/tools/cli/helpers/CommandTestBase.java b/tools/all/src/test/java/org/apache/bookkeeper/tools/cli/helpers/CommandTestBase.java
similarity index 100%
rename from bookkeeper-tools/src/test/java/org/apache/bookkeeper/tools/cli/helpers/CommandTestBase.java
rename to tools/all/src/test/java/org/apache/bookkeeper/tools/cli/helpers/CommandTestBase.java
diff --git a/bookkeeper-tools/src/test/java/org/apache/bookkeeper/tools/cli/helpers/DiscoveryCommandTest.java b/tools/all/src/test/java/org/apache/bookkeeper/tools/cli/helpers/DiscoveryCommandTest.java
similarity index 100%
rename from bookkeeper-tools/src/test/java/org/apache/bookkeeper/tools/cli/helpers/DiscoveryCommandTest.java
rename to tools/all/src/test/java/org/apache/bookkeeper/tools/cli/helpers/DiscoveryCommandTest.java
diff --git a/bookkeeper-tools/src/test/java/org/apache/bookkeeper/tools/cli/helpers/DiscoveryCommandTestBase.java b/tools/all/src/test/java/org/apache/bookkeeper/tools/cli/helpers/DiscoveryCommandTestBase.java
similarity index 100%
rename from bookkeeper-tools/src/test/java/org/apache/bookkeeper/tools/cli/helpers/DiscoveryCommandTestBase.java
rename to tools/all/src/test/java/org/apache/bookkeeper/tools/cli/helpers/DiscoveryCommandTestBase.java
diff --git a/tools/all/src/test/resources/log4j.properties b/tools/all/src/test/resources/log4j.properties
new file mode 100644
index 000000000..10ae6bfcb
--- /dev/null
+++ b/tools/all/src/test/resources/log4j.properties
@@ -0,0 +1,42 @@
+#
+#
+# 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.
+#
+#
+
+#
+# Bookkeeper Logging Configuration
+#
+
+# Format is "<default threshold> (, <appender>)+
+
+# DEFAULT: console appender only, level INFO
+bookkeeper.root.logger=INFO,CONSOLE
+log4j.rootLogger=${bookkeeper.root.logger}
+
+#
+# Log INFO level and above messages to the console
+#
+log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
+log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
+log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} - %-5p - [%t:%C{1}@%L] - %m%n
+
+#disable zookeeper logging
+log4j.logger.org.apache.zookeeper=OFF
+log4j.logger.org.apache.bookkeeper.bookie=INFO
+log4j.logger.org.apache.bookkeeper.meta=INFO
diff --git a/tools/framework/pom.xml b/tools/framework/pom.xml
new file mode 100644
index 000000000..516e3f809
--- /dev/null
+++ b/tools/framework/pom.xml
@@ -0,0 +1,44 @@
+<?xml version="1.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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <artifactId>bookkeeper-tools-parent</artifactId>
+    <groupId>org.apache.bookkeeper</groupId>
+    <version>4.8.0-SNAPSHOT</version>
+  </parent>
+  <artifactId>bookkeeper-tools-framework</artifactId>
+  <name>Apache BookKeeper :: Tools :: Framework</name>
+  <dependencies>
+    <dependency>
+      <groupId>com.beust</groupId>
+      <artifactId>jcommander</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.bookkeeper</groupId>
+      <artifactId>bookkeeper-common</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.bookkeeper</groupId>
+      <artifactId>buildtools</artifactId>
+      <version>${project.parent.version}</version>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+</project>
diff --git a/tools/framework/src/main/java/org/apache/bookkeeper/tools/common/BKCommand.java b/tools/framework/src/main/java/org/apache/bookkeeper/tools/common/BKCommand.java
new file mode 100644
index 000000000..2f2724b92
--- /dev/null
+++ b/tools/framework/src/main/java/org/apache/bookkeeper/tools/common/BKCommand.java
@@ -0,0 +1,91 @@
+/*
+ * 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.bookkeeper.tools.common;
+
+import com.google.common.base.Strings;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.file.Paths;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.bookkeeper.common.net.ServiceURI;
+import org.apache.bookkeeper.tools.framework.Cli;
+import org.apache.bookkeeper.tools.framework.CliCommand;
+import org.apache.bookkeeper.tools.framework.CliFlags;
+import org.apache.bookkeeper.tools.framework.CliSpec;
+import org.apache.commons.configuration.CompositeConfiguration;
+import org.apache.commons.configuration.ConfigurationException;
+import org.apache.commons.configuration.PropertiesConfiguration;
+
+/**
+ * Base bk command class.
+ */
+@Slf4j
+public abstract class BKCommand<CommandFlagsT extends CliFlags> extends CliCommand<BKFlags, CommandFlagsT> {
+
+    protected BKCommand(CliSpec<CommandFlagsT> spec) {
+        super(spec);
+    }
+
+    @Override
+    public Boolean apply(BKFlags globalFlags, String[] args) {
+        CliSpec<CommandFlagsT> newSpec = CliSpec.newBuilder(spec)
+            .withRunFunc(cmdFlags -> apply(globalFlags, cmdFlags))
+            .build();
+        return 0 == Cli.runCli(newSpec, args);
+    }
+
+    protected boolean apply(BKFlags bkFlags, CommandFlagsT cmdFlags) {
+        ServiceURI serviceURI = null;
+
+        if (null != bkFlags.serviceUri) {
+            serviceURI = ServiceURI.create(bkFlags.serviceUri);
+            if (!acceptServiceUri(serviceURI)) {
+                log.error("Unresolvable service uri by command '{}' : {}",
+                    path(), bkFlags.serviceUri);
+                return false;
+            }
+        }
+
+        CompositeConfiguration conf = new CompositeConfiguration();
+        if (!Strings.isNullOrEmpty(bkFlags.configFile)) {
+            try {
+                URL configFileUrl = Paths.get(bkFlags.configFile).toUri().toURL();
+                PropertiesConfiguration loadedConf = new PropertiesConfiguration(configFileUrl);
+                conf.addConfiguration(loadedConf);
+            } catch (MalformedURLException e) {
+                log.error("Could not open configuration file : {}", bkFlags.configFile, e);
+                throw new IllegalArgumentException(e);
+            } catch (ConfigurationException e) {
+                log.error("Malformed configuration file : {}", bkFlags.configFile, e);
+                throw new IllegalArgumentException(e);
+            }
+        }
+
+        return apply(serviceURI, conf, cmdFlags);
+    }
+
+    protected boolean acceptServiceUri(ServiceURI serviceURI) {
+        return true;
+    }
+
+    protected abstract boolean apply(ServiceURI serviceURI,
+                                     CompositeConfiguration conf,
+                                     CommandFlagsT cmdFlags);
+
+}
diff --git a/tools/framework/src/main/java/org/apache/bookkeeper/tools/common/BKFlags.java b/tools/framework/src/main/java/org/apache/bookkeeper/tools/common/BKFlags.java
new file mode 100644
index 000000000..890a3df0f
--- /dev/null
+++ b/tools/framework/src/main/java/org/apache/bookkeeper/tools/common/BKFlags.java
@@ -0,0 +1,43 @@
+/*
+ * 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.bookkeeper.tools.common;
+
+import com.beust.jcommander.Parameter;
+import org.apache.bookkeeper.tools.framework.CliFlags;
+
+/**
+ * Default BK flags.
+ */
+public final class BKFlags extends CliFlags {
+
+    @Parameter(
+        names = {
+            "-u", "--service-uri"
+        },
+        description = "Service Uri")
+    public String serviceUri = null;
+
+    @Parameter(
+        names = {
+            "-c", "--conf"
+        },
+        description = "Configuration file")
+    public String configFile = null;
+
+}
diff --git a/tools/framework/src/main/java/org/apache/bookkeeper/tools/common/package-info.java b/tools/framework/src/main/java/org/apache/bookkeeper/tools/common/package-info.java
new file mode 100644
index 000000000..2268b6324
--- /dev/null
+++ b/tools/framework/src/main/java/org/apache/bookkeeper/tools/common/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+/**
+ * Common classes used across multiple tools.
+ */
+package org.apache.bookkeeper.tools.common;
\ No newline at end of file
diff --git a/tools/framework/src/main/java/org/apache/bookkeeper/tools/framework/Cli.java b/tools/framework/src/main/java/org/apache/bookkeeper/tools/framework/Cli.java
new file mode 100644
index 000000000..44b210ff2
--- /dev/null
+++ b/tools/framework/src/main/java/org/apache/bookkeeper/tools/framework/Cli.java
@@ -0,0 +1,250 @@
+/*
+ * 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.bookkeeper.tools.framework;
+
+import com.beust.jcommander.JCommander;
+import com.google.common.base.Strings;
+import java.io.PrintStream;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.function.Function;
+import java.util.stream.IntStream;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.bookkeeper.tools.framework.CliSpec.Builder;
+
+/**
+ * Cli to execute {@link CliSpec}.
+ */
+@Slf4j
+public class Cli<CliFlagsT extends CliFlags> {
+
+    private final CliSpec<CliFlagsT> spec;
+    private final JCommander commander;
+    @Getter(AccessLevel.PACKAGE)
+    private final Map<String, Command> commandMap;
+    private final Function<CliFlagsT, Boolean> runFunc;
+    private final String cmdPath;
+    private final PrintStream console;
+
+    public Cli(CliSpec<CliFlagsT> spec) {
+        this.spec = updateSpecWithDefaultValues(spec);
+        this.cmdPath = getCmdPath(spec);
+        this.commandMap = new TreeMap<>();
+        this.console = setupConsole(spec);
+        this.commander = setupCli(cmdPath, spec, commandMap);
+        if (null == spec.runFunc()) {
+            this.runFunc = (args) -> {
+                usage();
+                return false;
+            };
+        } else {
+            this.runFunc = spec.runFunc();
+        }
+
+        // inject default commands if needed
+        if (!spec.commands().isEmpty() && !hasCommand("help")) {
+            commandMap.put("help", new HelpCommand<>(this));
+        }
+    }
+
+    static <CliFlagsT extends CliFlags> String getCmdPath(CliSpec<CliFlagsT> spec) {
+        if (Strings.isNullOrEmpty(spec.parent())) {
+            return spec.name();
+        } else {
+            return spec.parent() + " " + spec.name();
+        }
+    }
+
+    static <CliFlagsT extends CliFlags> PrintStream setupConsole(CliSpec<CliFlagsT> spec) {
+        if (null == spec.console()) {
+            return System.out;
+        } else {
+            return spec.console();
+        }
+    }
+
+    CliSpec<CliFlagsT> updateSpecWithDefaultValues(CliSpec<CliFlagsT> spec) {
+        Builder<CliFlagsT> builder = CliSpec.newBuilder(spec);
+        if (Strings.isNullOrEmpty(spec.usage())) {
+            // simple command
+            if (spec.commands().isEmpty()) {
+                builder.withUsage(
+                    String.format(
+                        "%s [flags] " + spec.argumentsUsage(),
+                        getCmdPath(spec)));
+            // command group
+            } else {
+                builder.withUsage(
+                    String.format(
+                        "%s [flags] [command] [command options]",
+                        getCmdPath(spec)));
+            }
+        }
+
+        if (!spec.commands().isEmpty() && Strings.isNullOrEmpty(spec.tailer())) {
+            builder.withTailer(
+                String.format(
+                      "Use \"%s [command] --help\" or \"%s help [command]\" for "
+                    + "more information about a command",
+                    getCmdPath(spec), getCmdPath(spec)));
+        }
+        return builder.build();
+    }
+
+    String name() {
+        return spec.name();
+    }
+
+    String cmdPath() {
+        return cmdPath;
+    }
+
+    boolean hasCommand(String command) {
+        return commandMap.containsKey(command);
+    }
+
+    Command getCommand(String command) {
+        return commandMap.get(command);
+    }
+
+    static <CliFlagsT extends CliFlags> JCommander setupCli(String cmdPath,
+                                                            CliSpec<CliFlagsT> spec,
+                                                            Map<String, Command> commandMap) {
+        JCommander commander = new JCommander();
+        commander.setProgramName(cmdPath);
+        if (null != spec.flags()) {
+            commander.addObject(spec.flags());
+        }
+        for (Command cmd : spec.commands()) {
+            commandMap.put(cmd.name(), cmd);
+        }
+        // trigger command to generate help information
+        StringBuilder usageBuilder = new StringBuilder();
+        commander.usage(usageBuilder);
+        return commander;
+    }
+
+    protected void console(String msg) {
+        console().println(msg);
+    }
+
+    protected PrintStream console() {
+        return console;
+    }
+
+    void usage() {
+        usage(null);
+    }
+
+    void usage(String errorMsg) {
+        if (Strings.isNullOrEmpty(errorMsg)) {
+            CommandUtils.printDescription(console(), 0, 0, spec.description());
+            console("");
+        } else {
+            CommandUtils.printDescription(console(), 0, 0, "Error : " + errorMsg);
+            console("");
+        }
+
+        // usage section
+        CommandUtils.printUsage(console(), spec.usage());
+
+        // command section
+        CommandUtils.printAvailableCommands(commandMap, console());
+
+        // flags section
+        if (!spec.isCommandGroup()) {
+            CommandUtils.printAvailableFlags(commander, console());
+        }
+
+        if (!spec.commands().isEmpty()) {
+            // tailer section
+            CommandUtils.printDescription(console(), 0, 0, spec.tailer());
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    boolean run(String[] args) {
+        // the cli has commands and args is empty
+        if (!spec.commands().isEmpty() && args.length == 0) {
+            usage();
+            return false;
+        }
+
+        int cmdPos = IntStream.range(0, args.length)
+            .filter(pos -> commandMap.containsKey(args[pos]))
+            .findFirst()
+            .orElse(args.length);
+
+        String[] flagArgs = Arrays.copyOfRange(args, 0, cmdPos);
+
+        // if this cli is a sub-command group, the flag args should be empty
+        if (spec.isCommandGroup() && flagArgs.length > 0) {
+            usage();
+            return false;
+        }
+
+        // parse flags
+        if (flagArgs.length != 0) {
+            // skip parsing flags
+            try {
+                commander.parse(flagArgs);
+            } catch (Exception e) {
+                usage(e.getMessage());
+                return false;
+            }
+        }
+
+        // print help messages
+        if (spec.flags().help) {
+            usage();
+            return false;
+        }
+
+        // found a sub command
+        if (cmdPos == args.length) {
+            return runFunc.apply(spec.flags());
+        } else {
+            String cmd = args[cmdPos];
+            Command command = commandMap.get(cmd);
+            String[] subCmdArgs = Arrays.copyOfRange(
+                args, cmdPos + 1, args.length);
+
+            try {
+                return command.apply(spec.flags(), subCmdArgs);
+            } catch (Exception e) {
+                usage(e.getMessage());
+                return false;
+            }
+        }
+    }
+
+    public static <CliOptsT extends CliFlags> int runCli(
+            CliSpec<CliOptsT> spec, String[] args) {
+        Cli<CliOptsT> cli = new Cli<>(spec);
+        return cli.run(args) ? 0 : -1;
+    }
+
+    public static <CliOptsT extends CliFlags> void printUsage(CliSpec<CliOptsT> spec) {
+        Cli<CliOptsT> cli = new Cli<>(spec);
+        cli.usage();
+    }
+}
diff --git a/tools/framework/src/main/java/org/apache/bookkeeper/tools/framework/CliCommand.java b/tools/framework/src/main/java/org/apache/bookkeeper/tools/framework/CliCommand.java
new file mode 100644
index 000000000..ab7b955d1
--- /dev/null
+++ b/tools/framework/src/main/java/org/apache/bookkeeper/tools/framework/CliCommand.java
@@ -0,0 +1,70 @@
+/*
+ * 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.bookkeeper.tools.framework;
+
+/**
+ * A command that runs a CLI spec. it is typically used for nested sub commands.
+ */
+public class CliCommand<GlobalFlagsT extends CliFlags, CommandFlagsT extends CliFlags>
+        implements Command<GlobalFlagsT> {
+
+    private final String name;
+    private String parent;
+    protected CliSpec<CommandFlagsT> spec;
+
+    public CliCommand(CliSpec<CommandFlagsT> spec) {
+        this.spec = spec;
+        this.name = spec.name();
+        this.parent = spec.parent();
+    }
+
+    public void setParent(String parent) {
+        this.parent = parent;
+        this.spec = CliSpec.newBuilder(spec)
+            .withParent(parent)
+            .build();
+    }
+
+    @Override
+    public String name() {
+        return name;
+    }
+
+    @Override
+    public String path() {
+        return parent + " " + name;
+    }
+
+    @Override
+    public String description() {
+        return spec.description();
+    }
+
+    @Override
+    public Boolean apply(GlobalFlagsT globalFlags, String[] args) {
+        int res = Cli.runCli(spec, args);
+        return res == 0;
+    }
+
+    @Override
+    public void usage() {
+        // run with "empty args", which will print the usage for this command group.
+        Cli.printUsage(spec);
+    }
+}
diff --git a/tools/framework/src/main/java/org/apache/bookkeeper/tools/framework/CliCommandGroup.java b/tools/framework/src/main/java/org/apache/bookkeeper/tools/framework/CliCommandGroup.java
new file mode 100644
index 000000000..efe8fdd2f
--- /dev/null
+++ b/tools/framework/src/main/java/org/apache/bookkeeper/tools/framework/CliCommandGroup.java
@@ -0,0 +1,80 @@
+/*
+ * 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.bookkeeper.tools.framework;
+
+import com.google.common.base.Strings;
+
+/**
+ * A command group that group commands together. They share same global flags.
+ */
+public abstract class CliCommandGroup<GlobalFlagsT extends CliFlags>
+        extends CliCommand<GlobalFlagsT, GlobalFlagsT> implements CommandGroup<GlobalFlagsT> {
+
+    @SuppressWarnings("unchecked")
+    private static <GlobalFlagsT extends CliFlags> CliSpec<GlobalFlagsT> updateSpec(CliSpec<GlobalFlagsT> spec) {
+        CliSpec<GlobalFlagsT> newSpec;
+        if (Strings.isNullOrEmpty(spec.usage())) {
+            newSpec = CliSpec.newBuilder(spec)
+                .withUsage(String.format("%s %s [command] [command options]",
+                    spec.parent(), spec.name()
+                ))
+                .withFlags(null)
+                .build();
+        } else {
+            newSpec = CliSpec.newBuilder(spec)
+                .withFlags(null)
+                .build();
+        }
+
+        String path = newSpec.parent() + " " + newSpec.name();
+
+        for (Command<GlobalFlagsT> cmd : newSpec.commands()) {
+            if (cmd instanceof CliCommand) {
+                CliCommand<GlobalFlagsT, GlobalFlagsT> cliCmd = (CliCommand<GlobalFlagsT, GlobalFlagsT>) cmd;
+                cliCmd.setParent(path);
+            }
+        }
+
+        return newSpec;
+    }
+
+    protected CliCommandGroup(CliSpec<GlobalFlagsT> spec) {
+        super(updateSpec(spec));
+    }
+
+    @Override
+    public Boolean apply(GlobalFlagsT globalFlags, String[] args) {
+        CliSpec<GlobalFlagsT> newSpec = CliSpec.newBuilder(spec)
+            .withFlags(globalFlags)
+            .setCommandGroup(true)
+            .build();
+
+        return 0 == Cli.runCli(newSpec, args);
+    }
+
+    @Override
+    public void usage() {
+        CliSpec<GlobalFlagsT> newSpec = CliSpec.newBuilder(spec)
+            .setCommandGroup(true)
+            .build();
+
+        // run with "empty args", which will print the usage for this command group.
+        Cli.printUsage(newSpec);
+    }
+}
diff --git a/tools/framework/src/main/java/org/apache/bookkeeper/tools/framework/CliFlags.java b/tools/framework/src/main/java/org/apache/bookkeeper/tools/framework/CliFlags.java
new file mode 100644
index 000000000..3bff70d6f
--- /dev/null
+++ b/tools/framework/src/main/java/org/apache/bookkeeper/tools/framework/CliFlags.java
@@ -0,0 +1,43 @@
+/*
+ * 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.bookkeeper.tools.framework;
+
+
+import com.beust.jcommander.Parameter;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Default CLI Options.
+ */
+@SuppressFBWarnings("URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
+public class CliFlags {
+
+    @Parameter(description = "args")
+    public List<String> arguments = new ArrayList<>();
+
+    @Parameter(
+        names = {
+            "-h", "--help"
+        },
+        description = "Display help information")
+    public boolean help = false;
+
+}
diff --git a/tools/framework/src/main/java/org/apache/bookkeeper/tools/framework/CliSpec.java b/tools/framework/src/main/java/org/apache/bookkeeper/tools/framework/CliSpec.java
new file mode 100644
index 000000000..bbae3f581
--- /dev/null
+++ b/tools/framework/src/main/java/org/apache/bookkeeper/tools/framework/CliSpec.java
@@ -0,0 +1,242 @@
+/*
+ * 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.bookkeeper.tools.framework;
+
+import com.google.common.collect.Sets;
+import java.io.PrintStream;
+import java.util.Set;
+import java.util.function.Function;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+/**
+ * A spec to build CLI.
+ */
+@ToString
+@EqualsAndHashCode
+public class CliSpec<CliFlagsT extends CliFlags> {
+
+    /**
+     * Create a new builder to build the cli spec.
+     *
+     * @return a new builder to build the cli spec.
+     */
+    public static <T extends CliFlags> Builder<T> newBuilder() {
+        return new Builder<>();
+    }
+
+    /**
+     * Create a new builder to build the cli spec from an existing <tt>spec</tt>.
+     *
+     * @param spec cli spec
+     * @return a new builder to build the cli spec from an existing <tt>spec</tt>.
+     */
+    public static <T extends CliFlags> Builder<T> newBuilder(CliSpec<T> spec) {
+        return new Builder<>(spec);
+    }
+
+    /**
+     * Builder to build a cli spec.
+     */
+    public static class Builder<CliFlagsT extends CliFlags> {
+
+        private String name = "unknown";
+        private String parent = "";
+        private String usage = "";
+        private CliFlagsT flags = null;
+        private String description = "unset";
+        private final Set<Command> commands = Sets.newHashSet();
+        private String tailer = "";
+        private Function<CliFlagsT, Boolean> runFunc = null;
+        private PrintStream console = System.out;
+        private boolean isCommandGroup = false;
+        private String argumentsUsage = "";
+
+        private Builder() {}
+
+        private Builder(CliSpec<CliFlagsT> spec) {
+            this.name = spec.name;
+            this.parent = spec.parent;
+            this.usage = spec.usage;
+            this.argumentsUsage = spec.argumentsUsage;
+            this.flags = spec.flags;
+            this.description = spec.description;
+            this.commands.clear();
+            this.commands.addAll(spec.commands);
+            this.tailer = spec.tailer;
+            this.runFunc = spec.runFunc;
+            this.console = spec.console;
+            this.isCommandGroup = spec.isCommandGroup;
+        }
+
+        public Builder<CliFlagsT> withName(String name) {
+            this.name = name;
+            return this;
+        }
+
+        public Builder<CliFlagsT> withParent(String parent) {
+            this.parent = parent;
+            return this;
+        }
+
+        public Builder<CliFlagsT> withUsage(String usage) {
+            this.usage = usage;
+            return this;
+        }
+
+        public Builder<CliFlagsT> withArgumentsUsage(String usage) {
+            this.argumentsUsage = usage;
+            return this;
+        }
+
+        public Builder<CliFlagsT> withFlags(CliFlagsT flags) {
+            this.flags = flags;
+            return this;
+        }
+
+        public Builder<CliFlagsT> withDescription(String description) {
+            this.description = description;
+            return this;
+        }
+
+        public Builder<CliFlagsT> addCommand(Command command) {
+            this.commands.add(command);
+            return this;
+        }
+
+        public Builder<CliFlagsT> withTailer(String tailer) {
+            this.tailer = tailer;
+            return this;
+        }
+
+        public Builder<CliFlagsT> withRunFunc(Function<CliFlagsT, Boolean> func) {
+            this.runFunc = func;
+            return this;
+        }
+
+        public Builder<CliFlagsT> withConsole(PrintStream console) {
+            this.console = console;
+            return this;
+        }
+
+        public Builder<CliFlagsT> setCommandGroup(boolean enabled) {
+            this.isCommandGroup = enabled;
+            return this;
+        }
+
+        public CliSpec<CliFlagsT> build() {
+            return new CliSpec<>(
+                name,
+                parent,
+                usage,
+                argumentsUsage,
+                flags,
+                description,
+                commands,
+                tailer,
+                runFunc,
+                console,
+                isCommandGroup
+            );
+        }
+
+    }
+
+    private final String name;
+    private final String parent;
+    private final String usage;
+    private final String argumentsUsage;
+    private final CliFlagsT flags;
+    private final String description;
+    private final Set<Command> commands;
+    private final String tailer;
+    private final Function<CliFlagsT, Boolean> runFunc;
+    private final PrintStream console;
+    // whether the cli spec is for a command group.
+    private final boolean isCommandGroup;
+
+    private CliSpec(String name,
+                    String parent,
+                    String usage,
+                    String argumentsUsage,
+                    CliFlagsT flags,
+                    String description,
+                    Set<Command> commands,
+                    String tailer,
+                    Function<CliFlagsT, Boolean> runFunc,
+                    PrintStream console,
+                    boolean isCommandGroup) {
+        this.name = name;
+        this.parent = parent;
+        this.usage = usage;
+        this.flags = flags;
+        this.argumentsUsage = argumentsUsage;
+        this.description = description;
+        this.commands = commands;
+        this.tailer = tailer;
+        this.runFunc = runFunc;
+        this.console = console;
+        this.isCommandGroup = isCommandGroup;
+    }
+
+    public String name() {
+        return name;
+    }
+
+    public String parent() {
+        return parent;
+    }
+
+    public String usage() {
+        return usage;
+    }
+
+    public String argumentsUsage() {
+        return argumentsUsage;
+    }
+
+    public CliFlagsT flags() {
+        return flags;
+    }
+
+    public String description() {
+        return description;
+    }
+
+    public Set<Command> commands() {
+        return commands;
+    }
+
+    public String tailer() {
+        return tailer;
+    }
+
+    public Function<CliFlagsT, Boolean> runFunc() {
+        return runFunc;
+    }
+
+    public PrintStream console() {
+        return console;
+    }
+
+    public boolean isCommandGroup() {
+        return isCommandGroup;
+    }
+
+}
diff --git a/tools/framework/src/main/java/org/apache/bookkeeper/tools/framework/Command.java b/tools/framework/src/main/java/org/apache/bookkeeper/tools/framework/Command.java
new file mode 100644
index 000000000..186b29fad
--- /dev/null
+++ b/tools/framework/src/main/java/org/apache/bookkeeper/tools/framework/Command.java
@@ -0,0 +1,73 @@
+/*
+ * 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.bookkeeper.tools.framework;
+
+/**
+ * Command of a cli.
+ */
+public interface Command<GlobalFlagsT extends CliFlags> {
+
+    /**
+     * Returns whether to hide this command from showing in help message.
+     *
+     * @return true if hide this command from help message.
+     */
+    default boolean hidden() {
+        return false;
+    }
+
+    /**
+     * Return command name.
+     *
+     * @return command name.
+     */
+    String name();
+
+    /**
+     * Return command path in a cli path.
+     *
+     * <p>This is used for printing usage information.
+     *
+     * @return command path
+     */
+    default String path() {
+        return name();
+    }
+
+    /**
+     * Return description name.
+     *
+     * @return description name.
+     */
+    String description();
+
+    /**
+     * Process the command.
+     *
+     * @param args command args
+     * @return true if successfully apply the args, otherwise false
+     */
+    Boolean apply(GlobalFlagsT globalFlagsT,
+                  String[] args) throws Exception;
+
+    /**
+     * Print the help information.
+     */
+    void usage();
+}
diff --git a/tools/framework/src/main/java/org/apache/bookkeeper/tools/framework/CommandGroup.java b/tools/framework/src/main/java/org/apache/bookkeeper/tools/framework/CommandGroup.java
new file mode 100644
index 000000000..2e092f2df
--- /dev/null
+++ b/tools/framework/src/main/java/org/apache/bookkeeper/tools/framework/CommandGroup.java
@@ -0,0 +1,25 @@
+/*
+ * 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.bookkeeper.tools.framework;
+
+/**
+ * A command group that group commands together.
+ */
+public interface CommandGroup<GlobalFlagsT extends CliFlags> extends Command<GlobalFlagsT> {
+}
diff --git a/tools/framework/src/main/java/org/apache/bookkeeper/tools/framework/CommandUtils.java b/tools/framework/src/main/java/org/apache/bookkeeper/tools/framework/CommandUtils.java
new file mode 100644
index 000000000..77c6b48ae
--- /dev/null
+++ b/tools/framework/src/main/java/org/apache/bookkeeper/tools/framework/CommandUtils.java
@@ -0,0 +1,218 @@
+/*
+ * 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.bookkeeper.tools.framework;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.ParameterDescription;
+import com.beust.jcommander.WrappedParameter;
+import com.google.common.collect.Lists;
+import java.io.PrintStream;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.IntStream;
+
+/**
+ * Utils to process a commander.
+ */
+public class CommandUtils {
+
+    private static final int MAX_COLUMN_SIZE = 79;
+    private static final int DEFAULT_INDENT = 4;
+    private static final String USAGE_HEADER = "Usage:";
+
+    private static final Comparator<? super ParameterDescription> PD_COMPARATOR =
+        (Comparator<ParameterDescription>) (p0, p1) -> p0.getLongestName().compareTo(p1.getLongestName());
+
+    private static void printIndent(PrintStream printer, int indent) {
+        IntStream.range(0, indent).forEach(ignored -> printer.print(" "));
+    }
+
+    public static void printUsage(PrintStream printer, String usage) {
+        final int indent = ((USAGE_HEADER.length() / DEFAULT_INDENT) + 1) * DEFAULT_INDENT;
+        final int firstIndent = indent - USAGE_HEADER.length();
+        printer.print(USAGE_HEADER);
+        printDescription(
+            printer,
+            firstIndent,
+            indent,
+            usage);
+        printer.println();
+    }
+
+    /**
+     * Print the available flags in <tt>commander</tt>.
+     *
+     * @param commander commander
+     * @param printer printer
+     */
+    public static void printAvailableFlags(JCommander commander, PrintStream printer) {
+        List<ParameterDescription> sorted = Lists.newArrayList();
+        List<ParameterDescription> pds = commander.getParameters();
+
+        // Align the descriptions at the `longestName`
+        int longestName = 0;
+        for (ParameterDescription pd : pds) {
+           if (pd.getParameter().hidden()) {
+               continue;
+           }
+
+           sorted.add(pd);
+           int length = pd.getNames().length() + 2;
+           if (length > longestName) {
+               longestName = length;
+           }
+        }
+
+        if (sorted.isEmpty()) {
+            return;
+        }
+
+        // Sorted the flags
+        Collections.sort(sorted, PD_COMPARATOR);
+
+        // Display the flags
+        printer.println("Flags:");
+        printer.println();
+
+        ParameterDescription helpPd = null;
+
+        for (ParameterDescription pd : sorted) {
+            if ("--help".equals(pd.getLongestName())) {
+                helpPd = pd;
+                continue;
+            }
+
+            printFlag(pd, DEFAULT_INDENT, printer);
+            printer.println();
+        }
+
+        if (null != helpPd) {
+            printer.println();
+            printFlag(helpPd, DEFAULT_INDENT, printer);
+            printer.println();
+        }
+
+    }
+
+    private static void printFlag(ParameterDescription pd, int indent, PrintStream printer) {
+        WrappedParameter parameter = pd.getParameter();
+        // print flag
+        printIndent(printer, indent);
+        printer.print(pd.getNames());
+        printer.print(parameter.required() ? " (*)" : "");
+        printer.println();
+        // print flag description
+        int descIndent = 2 * indent;
+        printDescription(printer, descIndent, descIndent, pd.getDescription());
+    }
+
+    public static void printDescription(PrintStream printer,
+                                        int firstLineIndent,
+                                        int indent,
+                                        String description) {
+        int max = MAX_COLUMN_SIZE;
+        String[] words = description.split(" ");
+        int current = indent;
+        int i = 0;
+        printIndent(printer, firstLineIndent);
+        while (i < words.length) {
+            String word = words[i];
+            if (word.length() > max || current + word.length() <= max) {
+                if (i != 0) {
+                    printer.print(" ");
+                }
+                printer.print(word);
+                current += (word.length() + 1);
+            } else {
+                printer.println();
+                printIndent(printer, indent);
+                printer.print(word);
+                current = indent;
+            }
+            i++;
+        }
+        printer.println();
+    }
+
+    /**
+     * Print the available commands in <tt>commander</tt>.
+     *
+     * @param commands commands
+     * @param printer printer
+     */
+    public static void printAvailableCommands(Map<String, Command> commands,
+                                              PrintStream printer) {
+        if (commands.isEmpty()) {
+            return;
+        }
+
+        printer.println("Commands:");
+        printer.println();
+
+        int longestCommandName = commands
+            .keySet()
+            .stream()
+            .mapToInt(name -> name.length())
+            .max()
+            .orElse(0);
+
+        for (Map.Entry<String, Command> commandEntry : commands.entrySet()) {
+            if ("help".equals(commandEntry.getKey())) {
+                // don't print help message along with available other commands
+                continue;
+            }
+            printCommand(printer, commandEntry.getKey(), commandEntry.getValue(), longestCommandName);
+        }
+
+        Command helpCmd = commands.get("help");
+        if (null != helpCmd) {
+            printer.println();
+            printCommand(printer, "help", helpCmd, longestCommandName);
+        }
+
+        printer.println();
+    }
+
+    private static void printCommand(PrintStream printer,
+                                     String name,
+                                     Command command,
+                                     final int longestCommandName) {
+        if (command.hidden()) {
+            return;
+        }
+
+        final int indent = DEFAULT_INDENT;
+        final int startOfDescription =
+            (((indent + longestCommandName) / DEFAULT_INDENT) + 2) * DEFAULT_INDENT;
+
+        int current = 0;
+        printIndent(printer, indent);
+        printer.print(name);
+        current += (indent + name.length());
+        printIndent(printer, startOfDescription - current);
+        printDescription(
+            printer,
+            0,
+            startOfDescription,
+            command.description());
+    }
+
+}
diff --git a/tools/framework/src/main/java/org/apache/bookkeeper/tools/framework/HelpCommand.java b/tools/framework/src/main/java/org/apache/bookkeeper/tools/framework/HelpCommand.java
new file mode 100644
index 000000000..9981b7c35
--- /dev/null
+++ b/tools/framework/src/main/java/org/apache/bookkeeper/tools/framework/HelpCommand.java
@@ -0,0 +1,69 @@
+/*
+ * 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.bookkeeper.tools.framework;
+
+/**
+ * Help command to show help information.
+ */
+class HelpCommand<GlobalFlagsT extends CliFlags, CommandFlagsT extends CliFlags>
+        implements Command<GlobalFlagsT> {
+
+    private final Cli<CommandFlagsT> cli;
+
+    HelpCommand(Cli<CommandFlagsT> cli) {
+        this.cli = cli;
+    }
+
+    @Override
+    public String name() {
+        return "help";
+    }
+
+    @Override
+    public String description() {
+        return "Display help information about it";
+    }
+
+    @Override
+    public Boolean apply(GlobalFlagsT globalFlags, String[] args) throws Exception {
+        if (args.length == 0) {
+            cli.usage();
+            return true;
+        }
+
+        String cmd = args[0];
+        Command command = cli.getCommand(cmd);
+        if (null != command) {
+            command.usage();
+        } else {
+            cli.usage("Command \"" + cmd + "\" is not found.");
+        }
+
+        return true;
+    }
+
+    @Override
+    public void usage() {
+        cli.console("Help provides help for any command in this cli.");
+        cli.console("Simply type '" + cli.cmdPath() + " help [command]' for full details.");
+        cli.console("");
+        CommandUtils.printUsage(cli.console(), cli.cmdPath() + " help [command] [options]");
+    }
+
+}
diff --git a/tools/framework/src/main/java/org/apache/bookkeeper/tools/framework/package-info.java b/tools/framework/src/main/java/org/apache/bookkeeper/tools/framework/package-info.java
new file mode 100644
index 000000000..a488fd75b
--- /dev/null
+++ b/tools/framework/src/main/java/org/apache/bookkeeper/tools/framework/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+/**
+ * BookKeeper Tool Framework.
+ *
+ * <p>This package provides classes for writing various bookkeeper tools.
+ */
+package org.apache.bookkeeper.tools.framework;
\ No newline at end of file
diff --git a/tools/framework/src/test/java/org/apache/bookkeeper/tools/framework/CliTest.java b/tools/framework/src/test/java/org/apache/bookkeeper/tools/framework/CliTest.java
new file mode 100644
index 000000000..150f551f1
--- /dev/null
+++ b/tools/framework/src/test/java/org/apache/bookkeeper/tools/framework/CliTest.java
@@ -0,0 +1,274 @@
+/*
+ * 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.bookkeeper.tools.framework;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.Lists;
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.util.concurrent.CompletableFuture;
+import org.apache.bookkeeper.common.concurrent.FutureUtils;
+import org.apache.bookkeeper.tools.framework.TestCli.TestNestedFlags;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Testing the Cli framework using {@link TestCli}.
+ */
+public class CliTest {
+
+    private ByteArrayOutputStream consoleBuffer;
+    private PrintStream console;
+
+    @Before
+    public void setup() throws Exception {
+        this.consoleBuffer = new ByteArrayOutputStream();
+        this.console = new PrintStream(consoleBuffer, true, UTF_8.name());
+    }
+
+    @Test
+    public void testEmptyArgs() {
+        assertEquals(-1, TestCli.doMain(console, new String[] {}));
+
+        String consoleBufferStr = new String(consoleBuffer.toByteArray(), UTF_8);
+
+        assertTrue(consoleBufferStr.contains("Usage:"));
+        assertTrue(consoleBufferStr.contains("Commands:"));
+        assertTrue(consoleBufferStr.contains("Flags:"));
+    }
+
+    @Test
+    public void testHelpCommand() {
+        assertEquals(0, TestCli.doMain(console, new String[] {
+            "help"
+        }));
+
+        String consoleBufferStr = new String(consoleBuffer.toByteArray(), UTF_8);
+
+        assertTrue(consoleBufferStr.contains("Usage:"));
+        assertTrue(consoleBufferStr.contains("Commands:"));
+        assertTrue(consoleBufferStr.contains("Flags:"));
+    }
+
+    @Test
+    public void testHelpFlag() throws Exception {
+        assertEquals(0, TestCli.doMain(console, new String[] {
+            "help"
+        }));
+        String buffer1 = new String(consoleBuffer.toByteArray(), UTF_8);
+
+        ByteArrayOutputStream anotherBuffer = new ByteArrayOutputStream();
+        PrintStream anotherConsole = new PrintStream(anotherBuffer, true, UTF_8.name());
+
+        assertEquals(-1, TestCli.doMain(anotherConsole, new String[] {
+            "-h"
+        }));
+        String buffer2 = new String(anotherBuffer.toByteArray(), UTF_8);
+
+        assertEquals(buffer1, buffer2);
+    }
+
+    @Test
+    public void testPrintCommandUsage() throws Exception {
+        assertEquals(0, TestCli.doMain(console, new String[] {
+            "help", "dog"
+        }));
+
+        String consoleBufferStr = new String(consoleBuffer.toByteArray(), UTF_8);
+
+        ByteArrayOutputStream expectedBufferStream = new ByteArrayOutputStream();
+        PrintStream expectedConsole = new PrintStream(expectedBufferStream, true, UTF_8.name());
+        expectedConsole.println("command dog");
+        String expectedBuffer = new String(expectedBufferStream.toByteArray(), UTF_8);
+
+        assertEquals(expectedBuffer, consoleBufferStr);
+    }
+
+    @Test
+    public void testPrintHelpCommandUsage() throws Exception {
+        assertEquals(0, TestCli.doMain(console, new String[] {
+            "help", "help"
+        }));
+
+        String consoleBufferStr = new String(consoleBuffer.toByteArray(), UTF_8);
+
+        assertTrue(consoleBufferStr.contains(
+            "Usage:  bktest help [command] [options]"
+        ));
+    }
+
+    @Test
+    public void testInvalidFlags() throws Exception {
+        assertEquals(-1, TestCli.doMain(console, new String[] {
+            "-s", "string",
+            "-i", "1234",
+            "-l"
+        }));
+
+        String consoleBufferStr = new String(consoleBuffer.toByteArray(), UTF_8);
+
+        assertTrue(consoleBufferStr.contains("Error : Expected a value after parameter -l"));
+        // help message should be printed
+        assertTrue(consoleBufferStr.contains("Usage:"));
+        assertTrue(consoleBufferStr.contains("Commands:"));
+        assertTrue(consoleBufferStr.contains("Flags:"));
+    }
+
+    @Test
+    public void testNestedCommandEmptySubCommand() {
+        assertEquals(-1, TestCli.doMain(console, new String[] {
+            "nested"
+        }));
+
+        String consoleBufferStr = new String(consoleBuffer.toByteArray(), UTF_8);
+
+        // should print usage of 'nested' command
+        assertTrue(consoleBufferStr.contains("Usage:  bktest nested"));
+        assertTrue(consoleBufferStr.contains("Commands:"));
+        assertTrue(consoleBufferStr.contains("cat"));
+        assertTrue(consoleBufferStr.contains("fish"));
+        assertTrue(consoleBufferStr.contains("Flags:"));
+        assertTrue(consoleBufferStr.contains("--nested-int-flag"));
+    }
+
+    @Test
+    public void testNestedCommandHelpCommand() {
+        assertEquals(0, TestCli.doMain(console, new String[] {
+            "nested", "help"
+        }));
+
+        String consoleBufferStr = new String(consoleBuffer.toByteArray(), UTF_8);
+
+        // should print usage of 'nested' command
+        assertTrue(consoleBufferStr.contains("Usage:  bktest nested"));
+        assertTrue(consoleBufferStr.contains("Commands:"));
+        assertTrue(consoleBufferStr.contains("cat"));
+        assertTrue(consoleBufferStr.contains("fish"));
+        assertTrue(consoleBufferStr.contains("Flags:"));
+        assertTrue(consoleBufferStr.contains("--nested-int-flag"));
+    }
+
+    @Test
+    public void testNestedCommandHelpFlag() throws Exception {
+        assertEquals(0, TestCli.doMain(console, new String[] {
+            "nested", "help"
+        }));
+        String buffer1 = new String(consoleBuffer.toByteArray(), UTF_8);
+
+        ByteArrayOutputStream anotherBuffer = new ByteArrayOutputStream();
+        PrintStream anotherConsole = new PrintStream(anotherBuffer, true, UTF_8.name());
+
+        assertEquals(-1, TestCli.doMain(anotherConsole, new String[] {
+            "nested", "-h"
+        }));
+        String buffer2 = new String(anotherBuffer.toByteArray(), UTF_8);
+
+        assertEquals(buffer1, buffer2);
+    }
+
+    @Test
+    public void testPrintSubCommandUsage() throws Exception {
+        assertEquals(0, TestCli.doMain(console, new String[] {
+            "nested", "help", "cat"
+        }));
+
+        String consoleBufferStr = new String(consoleBuffer.toByteArray(), UTF_8);
+
+        ByteArrayOutputStream expectedBufferStream = new ByteArrayOutputStream();
+        PrintStream expectedConsole = new PrintStream(expectedBufferStream, true, UTF_8.name());
+        expectedConsole.println("command cat");
+        String expectedBuffer = new String(expectedBufferStream.toByteArray(), UTF_8);
+
+        assertEquals(expectedBuffer, consoleBufferStr);
+    }
+
+    @Test
+    public void testPrintHelpSubCommandUsage() throws Exception {
+        assertEquals(0, TestCli.doMain(console, new String[] {
+            "nested", "help", "help"
+        }));
+
+        String consoleBufferStr = new String(consoleBuffer.toByteArray(), UTF_8);
+
+        assertTrue(consoleBufferStr.contains(
+            "Usage:  bktest nested help [command] [options]"
+        ));
+    }
+
+    @Test
+    public void testSubCommandInvalidFlags() throws Exception {
+        assertEquals(-1, TestCli.doMain(console, new String[] {
+            "nested",
+            "-s", "string",
+            "-i", "1234",
+            "-l"
+        }));
+
+        String consoleBufferStr = new String(consoleBuffer.toByteArray(), UTF_8);
+
+        assertTrue(consoleBufferStr.contains("Error : Expected a value after parameter -l"));
+        // help message should be printed
+        assertTrue(consoleBufferStr.contains("Usage:  bktest nested"));
+        assertTrue(consoleBufferStr.contains("Commands:"));
+        assertTrue(consoleBufferStr.contains("cat"));
+        assertTrue(consoleBufferStr.contains("fish"));
+        assertTrue(consoleBufferStr.contains("Flags:"));
+        assertTrue(consoleBufferStr.contains("--nested-int-flag"));
+    }
+
+    @Test
+    public void testSubCommandFlags() throws Exception {
+        CompletableFuture<TestNestedFlags> flagsFuture = FutureUtils.createFuture();
+        assertEquals(0, TestCli.doMain(console, new String[] {
+            "nested",
+            "-s", "string",
+            "-i", "1234",
+            "-l", "str1,str2,str3",
+            "additional-args"
+        }, (flags) -> flagsFuture.complete(flags)));
+
+        dumpConsole();
+
+        TestNestedFlags flags = FutureUtils.result(flagsFuture);
+        assertEquals("string", flags.stringFlag);
+        assertEquals(1234, flags.intFlag);
+        Assert.assertEquals(
+            Lists.newArrayList("str1", "str2", "str3"),
+            flags.listFlag
+        );
+        assertEquals(1, flags.arguments.size());
+        Assert.assertEquals(Lists.newArrayList("additional-args"), flags.arguments);
+    }
+
+    //
+    // Util functions
+    //
+
+    private void dumpConsole() {
+        String buffer = new String(
+            consoleBuffer.toByteArray(), UTF_8);
+
+        System.out.println(buffer);
+    }
+
+}
diff --git a/tools/framework/src/test/java/org/apache/bookkeeper/tools/framework/TestCli.java b/tools/framework/src/test/java/org/apache/bookkeeper/tools/framework/TestCli.java
new file mode 100644
index 000000000..3f5f678e0
--- /dev/null
+++ b/tools/framework/src/test/java/org/apache/bookkeeper/tools/framework/TestCli.java
@@ -0,0 +1,129 @@
+/*
+ * 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.bookkeeper.tools.framework;
+
+import com.beust.jcommander.Parameter;
+import com.google.common.collect.Lists;
+import java.io.PrintStream;
+import java.util.List;
+import java.util.function.Function;
+import org.apache.bookkeeper.tools.framework.commands.TestCommand;
+
+/**
+ * A CLI used for testing.
+ */
+public class TestCli {
+
+    private static final String NAME = "bktest";
+
+    private static final String DESC = "bookkeeper test";
+
+    static class TestFlags extends CliFlags {
+
+        @Parameter(
+            names = {
+                "-s", "--string-flag"
+            },
+            description = "string flag")
+        public String stringFlag = null;
+
+        @Parameter(
+            names = {
+                "-i", "--int-flag"
+            },
+            description = "int flag")
+        public int intFlag = 0;
+
+        @Parameter(
+            names = {
+                "-l", "--list-flags"
+            },
+            description = "list flag")
+        public List<String> listFlag = Lists.newArrayList();
+
+    }
+
+    static class TestNestedFlags extends CliFlags {
+
+        @Parameter(
+            names = {
+                "-s", "--nested-string-flag"
+            },
+            description = "string flag")
+        public String stringFlag = null;
+
+        @Parameter(
+            names = {
+                "-i", "--nested-int-flag"
+            },
+            description = "int flag")
+        public int intFlag = 0;
+
+        @Parameter(
+            names = {
+                "-l", "--nested-list-flags"
+            },
+            description = "list flag")
+        public List<String> listFlag = Lists.newArrayList();
+
+    }
+
+    public static void main(String[] args) {
+        Runtime.getRuntime().exit(
+            doMain(System.out, args));
+    }
+
+    static int doMain(PrintStream console, String[] args) {
+        return doMain(console, args, null);
+    }
+
+    static int doMain(PrintStream console,
+                      String[] args,
+                      Function<TestNestedFlags, Boolean> func) {
+        String nestedCommandName = "nested";
+        String nestedCommandDesc = "bookkeeper test-nested";
+        CliSpec<TestNestedFlags> nestedSpec = CliSpec.<TestNestedFlags>newBuilder()
+            .withName(nestedCommandName)
+            .withParent(NAME)
+            .withDescription(nestedCommandDesc)
+            .withFlags(new TestNestedFlags())
+            .withConsole(console)
+            .addCommand(new TestCommand("fish", console))
+            .addCommand(new TestCommand("cat", console))
+            .withRunFunc(func)
+            .build();
+        CliCommand<TestFlags, TestNestedFlags> nestedCommand =
+            new CliCommand<>(nestedSpec);
+
+        CliSpec<TestFlags> spec = CliSpec.<TestFlags>newBuilder()
+            .withName(NAME)
+            .withDescription(DESC)
+            .withFlags(new TestFlags())
+            .withConsole(console)
+            .addCommand(new TestCommand("monkey", console))
+            .addCommand(new TestCommand("dog", console))
+            .addCommand(nestedCommand)
+            .build();
+
+        return Cli.runCli(spec, args);
+    }
+
+
+
+}
diff --git a/tools/framework/src/test/java/org/apache/bookkeeper/tools/framework/commands/TestCommand.java b/tools/framework/src/test/java/org/apache/bookkeeper/tools/framework/commands/TestCommand.java
new file mode 100644
index 000000000..19f10e2cf
--- /dev/null
+++ b/tools/framework/src/test/java/org/apache/bookkeeper/tools/framework/commands/TestCommand.java
@@ -0,0 +1,58 @@
+/*
+ * 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.bookkeeper.tools.framework.commands;
+
+import java.io.PrintStream;
+import org.apache.bookkeeper.tools.framework.CliFlags;
+import org.apache.bookkeeper.tools.framework.Command;
+
+/**
+ * Test Command.
+ */
+public class TestCommand implements Command<CliFlags> {
+
+    private final String label;
+    private final PrintStream console;
+
+    public TestCommand(String label,
+                       PrintStream console) {
+        this.label = label;
+        this.console = console;
+    }
+
+    @Override
+    public String name() {
+        return label;
+    }
+
+    @Override
+    public String description() {
+        return "Command " + label;
+    }
+
+    @Override
+    public Boolean apply(CliFlags globalFlags, String[] args) {
+        return true;
+    }
+
+    @Override
+    public void usage() {
+        console.println("command " + label);
+    }
+}
diff --git a/tools/pom.xml b/tools/pom.xml
new file mode 100644
index 000000000..46b776390
--- /dev/null
+++ b/tools/pom.xml
@@ -0,0 +1,32 @@
+<?xml version="1.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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <artifactId>bookkeeper</artifactId>
+    <groupId>org.apache.bookkeeper</groupId>
+    <version>4.8.0-SNAPSHOT</version>
+  </parent>
+  <artifactId>bookkeeper-tools-parent</artifactId>
+  <name>Apache BookKeeper :: Tools :: Parent</name>
+  <packaging>pom</packaging>
+  <modules>
+    <module>framework</module>
+    <module>all</module>
+  </modules>
+</project>


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services