You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ratis.apache.org by sz...@apache.org on 2021/11/10 07:27:17 UTC
[ratis] branch master updated: RATIS-1425. Add ratis-shell sub
commands framework (#528)
This is an automated email from the ASF dual-hosted git repository.
szetszwo pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ratis.git
The following commit(s) were added to refs/heads/master by this push:
new 8fb9f7a RATIS-1425. Add ratis-shell sub commands framework (#528)
8fb9f7a is described below
commit 8fb9f7a64228a442af0a539614b03aa951ce1240
Author: maobaolong <30...@qq.com>
AuthorDate: Wed Nov 10 15:27:13 2021 +0800
RATIS-1425. Add ratis-shell sub commands framework (#528)
---
ratis-shell/pom.xml | 25 ++++
.../org/apache/ratis/shell/cli/AbstractShell.java | 149 +++++++++++++++++++++
.../java/org/apache/ratis/shell/cli/Command.java | 122 +++++++++++++++++
.../org/apache/ratis/shell/cli/sh/RatisShell.java | 62 ++++++++-
.../apache/ratis/shell/cli/sh/command/Context.java | 54 ++++++++
5 files changed, 410 insertions(+), 2 deletions(-)
diff --git a/ratis-shell/pom.xml b/ratis-shell/pom.xml
index e6305dc..1b3a26f 100644
--- a/ratis-shell/pom.xml
+++ b/ratis-shell/pom.xml
@@ -24,9 +24,34 @@
<name>Apache Ratis Shell</name>
<properties>
+ <guava.version>29.0-jre</guava.version>
</properties>
<dependencies>
+ <dependency>
+ <groupId>org.apache.ratis</groupId>
+ <artifactId>ratis-common</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>commons-cli</groupId>
+ <artifactId>commons-cli</artifactId>
+ <version>1.5.0</version>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-log4j12</artifactId>
+ <scope>runtime</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.reflections</groupId>
+ <artifactId>reflections</artifactId>
+ <version>0.10.2</version>
+ </dependency>
</dependencies>
<build>
<plugins>
diff --git a/ratis-shell/src/main/java/org/apache/ratis/shell/cli/AbstractShell.java b/ratis-shell/src/main/java/org/apache/ratis/shell/cli/AbstractShell.java
new file mode 100644
index 0000000..7a32c30
--- /dev/null
+++ b/ratis-shell/src/main/java/org/apache/ratis/shell/cli/AbstractShell.java
@@ -0,0 +1,149 @@
+/*
+ * 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.ratis.shell.cli;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.ratis.thirdparty.com.google.common.io.Closer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Map;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+/**
+ * Abstract class for handling command line inputs.
+ */
+public abstract class AbstractShell implements Closeable {
+ private static final Logger LOG = LoggerFactory.getLogger(AbstractShell.class);
+
+ private final Map<String, Command> mCommands;
+ private final Closer closer;
+
+ /**
+ * Creates a new instance of {@link AbstractShell}.
+ */
+ public AbstractShell() {
+ closer = Closer.create();
+ mCommands = loadCommands();
+ // Register all loaded commands under closer.
+ mCommands.values().forEach(closer::register);
+ }
+
+ /**
+ * Handles the specified shell command request, displaying usage if the command format is invalid.
+ *
+ * @param argv [] Array of arguments given by the user's input from the terminal
+ * @return 0 if command is successful, -1 if an error occurred
+ */
+ public int run(String... argv) {
+ if (argv.length == 0) {
+ printUsage();
+ return -1;
+ }
+
+ // Sanity check on the number of arguments
+ String cmd = argv[0];
+ Command command = mCommands.get(cmd);
+
+ if (command == null) {
+ // Unknown command (we didn't find the cmd in our dict)
+ System.err.printf("%s is an unknown command.%n", cmd);
+ printUsage();
+ return -1;
+ }
+
+ // Find the inner-most command and its argument line.
+ CommandLine cmdline;
+ try {
+ String[] currArgs = Arrays.copyOf(argv, argv.length);
+ while (command.hasSubCommand()) {
+ if (currArgs.length < 2) {
+ throw new IllegalArgumentException("No sub-command is specified");
+ }
+ if (!command.getSubCommands().containsKey(currArgs[1])) {
+ throw new IllegalArgumentException("Unknown sub-command: " + currArgs[1]);
+ }
+ command = command.getSubCommands().get(currArgs[1]);
+ currArgs = Arrays.copyOfRange(currArgs, 1, currArgs.length);
+ }
+ currArgs = Arrays.copyOfRange(currArgs, 1, currArgs.length);
+
+ cmdline = command.parseAndValidateArgs(currArgs);
+ } catch (IllegalArgumentException e) {
+ // It outputs a prompt message when passing wrong args to CLI
+ System.out.println(e.getMessage());
+ System.out.println("Usage: " + command.getUsage());
+ System.out.println(command.getDescription());
+ LOG.error("Invalid arguments for command {}:", command.getCommandName(), e);
+ return -1;
+ }
+
+ // Handle the command
+ try {
+ return command.run(cmdline);
+ } catch (Exception e) {
+ System.out.println(e.getMessage());
+ LOG.error("Error running" + Arrays.stream(argv).reduce("", (a, b) -> a + " " + b), e);
+ return -1;
+ }
+ }
+
+ /**
+ * @return all commands provided by this shell
+ */
+ public Collection<Command> getCommands() {
+ return mCommands.values();
+ }
+
+ @Override
+ public void close() throws IOException {
+ closer.close();
+ }
+
+ /**
+ * @return name of the shell
+ */
+ protected abstract String getShellName();
+
+ /**
+ * Map structure: Command name => {@link Command} instance.
+ *
+ * @return a set of commands which can be executed under this shell
+ */
+ protected abstract Map<String, Command> loadCommands();
+
+ protected Closer getCloser() {
+ return closer;
+ }
+
+ /**
+ * Prints usage for all commands.
+ */
+ protected void printUsage() {
+ System.out.println("Usage: ratis " + getShellName() + " [generic options]");
+ SortedSet<String> sortedCmds = new TreeSet<>(mCommands.keySet());
+ for (String cmd : sortedCmds) {
+ System.out.format("%-60s%n", "\t [" + mCommands.get(cmd).getUsage() + "]");
+ }
+ }
+}
diff --git a/ratis-shell/src/main/java/org/apache/ratis/shell/cli/Command.java b/ratis-shell/src/main/java/org/apache/ratis/shell/cli/Command.java
new file mode 100644
index 0000000..31b8688
--- /dev/null
+++ b/ratis-shell/src/main/java/org/apache/ratis/shell/cli/Command.java
@@ -0,0 +1,122 @@
+/*
+ * 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.ratis.shell.cli;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * An interface for all the commands that can be run from a shell.
+ */
+public interface Command extends Closeable {
+
+ /**
+ * Gets the command name as input from the shell.
+ *
+ * @return the command name
+ */
+ String getCommandName();
+
+ /**
+ * @return the supported {@link Options} of the command
+ */
+ default Options getOptions() {
+ return new Options();
+ }
+
+ /**
+ * If a command has sub-commands, the first argument should be the sub-command's name,
+ * all arguments and options will be parsed for the sub-command.
+ *
+ * @return whether this command has sub-commands
+ */
+ default boolean hasSubCommand() {
+ return false;
+ }
+
+ /**
+ * @return a map from sub-command names to sub-command instances
+ */
+ default Map<String, Command> getSubCommands() {
+ return Collections.emptyMap();
+ }
+
+ /**
+ * Parses and validates the arguments.
+ *
+ * @param args the arguments for the command, excluding the command name
+ * @return the parsed command line object
+ * @throws IllegalArgumentException when arguments are not valid
+ */
+ default CommandLine parseAndValidateArgs(String... args) throws IllegalArgumentException {
+ CommandLine cmdline;
+ Options opts = getOptions();
+ CommandLineParser parser = new DefaultParser();
+ try {
+ cmdline = parser.parse(opts, args);
+ } catch (ParseException e) {
+ throw new IllegalArgumentException(
+ String.format("Failed to parse args for %s: %s", getCommandName(), e.getMessage()), e);
+ }
+ validateArgs(cmdline);
+ return cmdline;
+ }
+
+ /**
+ * Checks if the arguments are valid or throw InvalidArgumentException.
+ *
+ * @param cl the parsed command line for the arguments
+ * @throws IllegalArgumentException when arguments are not valid
+ */
+ default void validateArgs(CommandLine cl) throws IllegalArgumentException {}
+
+ /**
+ * Runs the command.
+ *
+ * @param cl the parsed command line for the arguments
+ * @return the result of running the command
+ */
+ default int run(CommandLine cl) throws IOException {
+ return 0;
+ }
+
+ /**
+ * @return the usage information of the command
+ */
+ String getUsage();
+
+ /**
+ * @return the description information of the command
+ */
+ String getDescription();
+
+ /**
+ * Used to close resources created by commands.
+ *
+ * @throws IOException if closing resources fails
+ */
+ default void close() throws IOException {}
+}
diff --git a/ratis-shell/src/main/java/org/apache/ratis/shell/cli/sh/RatisShell.java b/ratis-shell/src/main/java/org/apache/ratis/shell/cli/sh/RatisShell.java
index 0a26b5e..eebef9e 100644
--- a/ratis-shell/src/main/java/org/apache/ratis/shell/cli/sh/RatisShell.java
+++ b/ratis-shell/src/main/java/org/apache/ratis/shell/cli/sh/RatisShell.java
@@ -17,8 +17,66 @@
*/
package org.apache.ratis.shell.cli.sh;
-public class RatisShell {
+import org.apache.ratis.shell.cli.AbstractShell;
+import org.apache.ratis.shell.cli.Command;
+import org.apache.ratis.shell.cli.sh.command.Context;
+import org.apache.ratis.util.ReflectionUtils;
+import org.reflections.Reflections;
+
+import java.lang.reflect.Modifier;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Shell for manage ratis group.
+ */
+public class RatisShell extends AbstractShell {
+
+ /**
+ * Manage ratis shell command.
+ *
+ * @param args array of arguments given by the user's input from the terminal
+ */
public static void main(String[] args) {
- System.out.println("Hello " + RatisShell.class.getSimpleName());
+ RatisShell extensionShell = new RatisShell();
+ System.exit(extensionShell.run(args));
+ }
+
+ @Override
+ protected String getShellName() {
+ return "sh";
+ }
+
+ @Override
+ protected Map<String, Command> loadCommands() {
+ Context adminContext = new Context(System.out);
+ return loadCommands(RatisShell.class.getPackage().getName(),
+ new Class[] {Context.class},
+ new Object[] {getCloser().register(adminContext)});
+ }
+
+ /**
+ * Get instances of all subclasses of {@link Command} in a sub-package called "command" the given
+ * package.
+ *
+ * @param pkgName package prefix to look in
+ * @param classArgs type of args to instantiate the class
+ * @param objectArgs args to instantiate the class
+ * @return a mapping from command name to command instance
+ */
+ public static Map<String, Command> loadCommands(String pkgName, Class[] classArgs,
+ Object[] objectArgs) {
+ Map<String, Command> commandsMap = new HashMap<>();
+ Reflections reflections = new Reflections(pkgName);
+ for (Class<? extends Command> cls : reflections.getSubTypesOf(Command.class)) {
+ // Add commands from <pkgName>.command.*
+ if (cls.getPackage().getName().equals(pkgName + ".command")
+ && !Modifier.isAbstract(cls.getModifiers())) {
+ // Only instantiate a concrete class
+ final Command cmd = ReflectionUtils.newInstance(cls, classArgs, objectArgs);
+ commandsMap.put(cmd.getCommandName(), cmd);
+ }
+ }
+ return commandsMap;
}
}
diff --git a/ratis-shell/src/main/java/org/apache/ratis/shell/cli/sh/command/Context.java b/ratis-shell/src/main/java/org/apache/ratis/shell/cli/sh/command/Context.java
new file mode 100644
index 0000000..bae98dc
--- /dev/null
+++ b/ratis-shell/src/main/java/org/apache/ratis/shell/cli/sh/command/Context.java
@@ -0,0 +1,54 @@
+/*
+ * 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.ratis.shell.cli.sh.command;
+
+import org.apache.ratis.thirdparty.com.google.common.io.Closer;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.util.Objects;
+
+/**
+ * A context for ratis-shell.
+ */
+public final class Context implements Closeable {
+ private final PrintStream mPrintStream;
+ private final Closer mCloser;
+
+ /**
+ * Build a context.
+ * @param printStream the print stream
+ */
+ public Context(PrintStream printStream) {
+ mCloser = Closer.create();
+ mPrintStream = mCloser.register(Objects.requireNonNull(printStream, "printStream == null"));
+ }
+
+ /**
+ * @return the print stream to write to
+ */
+ public PrintStream getPrintStream() {
+ return mPrintStream;
+ }
+
+ @Override
+ public void close() throws IOException {
+ mCloser.close();
+ }
+}