This is an automated email from the ASF dual-hosted git repository.
roryqi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-uniffle.git
The following commit(s) were added to refs/heads/master by this push:
new a3ca2585 [#770] feat(cli): Introduce apache.commons.cli basic
framework (#833)
a3ca2585 is described below
commit a3ca2585487265cf791d5a0c6f401041eb3f4b55
Author: slfan1989 <[email protected]>
AuthorDate: Thu Apr 27 18:58:05 2023 +0800
[#770] feat(cli): Introduce apache.commons.cli basic framework (#833)
### What changes were proposed in this pull request?
We use `apache.commons.cli` to add commands to `Uniffle`. I created a
command called `uniffle`, the following is the case of using the command.
> Case1: Use the uniffle command directly.
- uniffle / uniffle --help
```
Usage: uniffle [OPTIONS] SUBCOMMAND [SUBCOMMAND OPTIONS]
or uniffle [OPTIONS] CLASSNAME [CLASSNAME OPTIONS]
where CLASSNAME is a user-provided Java class
OPTIONS is none or any of:
--daemon (start|status|stop) operate on a daemon
SUBCOMMAND is one of:
Admin Commands:
admin-cli prints uniffle-admin args information
Client Commands:
client-cli prints uniffle-cli args information
SUBCOMMAND may print help when invoked w/o parameters or with -h.
```
> Case2: use a command that does not create.
- uniffle testadmin
```
ERROR: testadmin is not COMMAND nor fully qualified CLASSNAME.
DEBUG: UNIFFLE_SUBCMD_USAGE_TYPES accepted client
DEBUG: UNIFFLE_SUBCMD_USAGE_TYPES accepted admin
Usage: uniffle [OPTIONS] SUBCOMMAND [SUBCOMMAND OPTIONS]
or uniffle [OPTIONS] CLASSNAME [CLASSNAME OPTIONS]
where CLASSNAME is a user-provided Java class
OPTIONS is none or any of:
--daemon (start|status|stop) operate on a daemon
SUBCOMMAND is one of:
Admin Commands:
admin-cli prints uniffle-admin args information
Client Commands:
client-cli prints uniffle-cli args information
SUBCOMMAND may print help when invoked w/o parameters or with -h.
```
> Case3: Use the help command
- uniffle client-cli --help
```
Usage:
Optional
-a,--admin <arg> This is an admin command that will print args.
-c,--cli <arg> This is an client cli command that will print args.
-h,--help Help for the Uniffle CLI.
```
> Case4: Run the example-cli command
- uniffle client-cli --cli test-cli
```
uniffle-client-cli : test-cli
```
### Why are the changes needed?
We use apache.commons.cli to add commands to Uniffle.
### Does this PR introduce _any_ user-facing change?
No.
### How was this patch tested?
- Unit test verification.
- Execute commands directly.
Co-authored-by: slfan1989 <louj1988@@>
---
bin/uniffle | 81 +++++
bin/uniffle-function.sh | 375 +++++++++++++++++++++
bin/utils.sh | 21 +-
build_distribution.sh | 7 +
cli/pom.xml | 45 +++
.../apache/uniffle/AbstractCustomCommandLine.java | 81 +++++
.../java/org/apache/uniffle/CustomCommandLine.java | 27 ++
.../apache/uniffle/UniffleCliArgsException.java | 30 ++
.../java/org/apache/uniffle/cli/UniffleCLI.java | 96 ++++++
.../org/apache/uniffle/cli/UniffleTestCLI.java | 86 +++++
pom.xml | 8 +
11 files changed, 850 insertions(+), 7 deletions(-)
diff --git a/bin/uniffle b/bin/uniffle
new file mode 100755
index 00000000..5b6fdd7b
--- /dev/null
+++ b/bin/uniffle
@@ -0,0 +1,81 @@
+#!/usr/bin/env bash
+
+# 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.
+
+source "$(dirname "$0")/uniffle-function.sh"
+UNIFFLE_SHELL_EXECNAME="uniffle"
+
+function uniffle_usage
+{
+ uniffle_add_option "--daemon (start|status|stop)" "operate on a daemon"
+ uniffle_add_subcommand "client-cli" client "prints uniffle-cli args
information"
+ uniffle_add_subcommand "admin-cli" admin "prints uniffle-admin args
information"
+ uniffle_generate_usage "${UNIFFLE_SHELL_EXECNAME}" true
+}
+
+function uniffle_cmd_case
+{
+ subcmd=$1
+ shift
+
+ case ${subcmd} in
+ client-cli)
+ UNIFFLE_CLASSNAME=org.apache.uniffle.cli.UniffleCLI
+ ;;
+ admin-cli)
+ UNIFFLE_CLASSNAME=org.apache.uniffle.cli.UniffleCLI
+ ;;
+ *)
+ UNIFFLE_CLASSNAME="${subcmd}"
+ if ! uniffle_validate_classname "${UNIFFLE_CLASSNAME}"; then
+ uniffle_exit_with_usage 1
+ fi
+ ;;
+ esac
+}
+
+if [[ $# = 0 ]]; then
+ uniffle_exit_with_usage 1
+fi
+
+UNIFFLE_SUBCMD=$1
+shift
+
+case $UNIFFLE_SUBCMD in
+--help|-help|-h)
+ uniffle_exit_with_usage 0
+ exit
+ ;;
+esac
+
+UNIFFLE_SUBCMD_ARGS=("$@")
+
+source "$(dirname "$0")/utils.sh"
+UNIFFLE_SHELL_SCRIPT_DEBUG=false
+load_rss_env
+uniffle_java_setup
+
+CLASSPATH=""
+
+JAR_DIR="${RSS_HOME}/jars"
+for file in $(ls ${JAR_DIR}/cli/*.jar 2>/dev/null); do
+ CLASSPATH=$CLASSPATH:$file
+done
+
+set +u
+uniffle_cmd_case "${UNIFFLE_SUBCMD}" "${UNIFFLE_SUBCMD_ARGS[@]}"
+uniffle_generic_java_subcmd_handler
+set -u
diff --git a/bin/uniffle-function.sh b/bin/uniffle-function.sh
new file mode 100644
index 00000000..07dd615d
--- /dev/null
+++ b/bin/uniffle-function.sh
@@ -0,0 +1,375 @@
+#!/usr/bin/env bash
+
+# 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.
+
+declare -a UNIFFLE_SUBCMD_USAGE
+declare -a UNIFFLE_OPTION_USAGE
+declare -a UNIFFLE_SUBCMD_USAGE_TYPES
+
+#---
+# uniffle_array_contains: Check if an array has a given value.
+# @param1 element
+# @param2 array
+# @returns 0 = yes; 1 = no
+#---
+function uniffle_array_contains
+{
+ declare element=$1
+ shift
+ declare val
+
+ if [[ "$#" -eq 0 ]]; then
+ return 1
+ fi
+
+ for val in "${@}"; do
+ if [[ "${val}" == "${element}" ]]; then
+ return 0
+ fi
+ done
+ return 1
+}
+
+#---
+# uniffle_add_array_param: Add the `appendstring` if `checkstring` is not
present in the given array.
+# @param1 envvar
+# @param2 appendstring
+#---
+function uniffle_add_array_param
+{
+ declare arrname=$1
+ declare add=$2
+
+ declare arrref="${arrname}[@]"
+ declare array=("${!arrref}")
+
+ if ! uniffle_array_contains "${add}" "${array[@]}"; then
+ eval ${arrname}=\(\"\${array[@]}\" \"${add}\" \)
+ uniffle_debug "$1 accepted $2"
+ else
+ uniffle_debug "$1 declined $2"
+ fi
+}
+
+#---
+# uniffle_error: Print a message to stderr.
+# @param1 string
+#---
+function uniffle_error
+{
+ echo "$*" 1>&2
+}
+
+#---
+# uniffle_debug: Print a message to stderr if --debug is turned on.
+# @param1 string
+#---
+function uniffle_debug
+{
+ if [[ -n "${UNIFFLE_SHELL_SCRIPT_DEBUG}" ]]; then
+ echo "DEBUG: $*" 1>&2
+ fi
+}
+
+#---
+# uniffle_exit_with_usage: Print usage information and exit with the passed.
+# @param1 exitcode
+# @return This function will always exit.
+#---
+function uniffle_exit_with_usage
+{
+ local exitcode=$1
+ if [[ -z $exitcode ]]; then
+ exitcode=1
+ fi
+
+ if declare -F uniffle_usage >/dev/null ; then
+ uniffle_usage
+ else
+ uniffle_error "Sorry, no help available."
+ fi
+ exit $exitcode
+}
+
+#---
+# uniffle_sort_array: Sort an array (must not contain regexps) present in the
given array.
+# @param1 arrayvar
+#---
+function uniffle_sort_array
+{
+ declare arrname=$1
+ declare arrref="${arrname}[@]"
+ declare array=("${!arrref}")
+ declare oifs
+
+ declare globstatus
+ declare -a sa
+
+ globstatus=$(set -o | grep noglob | awk '{print $NF}')
+
+ set -f
+ oifs=${IFS}
+
+ IFS=$'\n' sa=($(sort <<<"${array[*]}"))
+
+ eval "${arrname}"=\(\"\${sa[@]}\"\)
+
+ IFS=${oifs}
+ if [[ "${globstatus}" = off ]]; then
+ set +f
+ fi
+}
+
+#---
+# uniffle_generate_usage: generate standard usage output and optionally takes
a class.
+# @param1 execname
+# @param2 true|false
+# @param3 [text to use in place of SUBCOMMAND]
+#---
+function uniffle_generate_usage
+{
+ declare cmd=$1
+ declare takesclass=$2
+ declare subcmdtext=${3:-"SUBCOMMAND"}
+ declare haveoptions
+ declare optstring
+ declare havesubs
+ declare subcmdstring
+ declare cmdtype
+
+ cmd=${cmd##*/}
+
+ if [[ -n "${UNIFFLE_OPTION_USAGE_COUNTER}"
+ && "${UNIFFLE_OPTION_USAGE_COUNTER}" -gt 0 ]]; then
+ haveoptions=true
+ optstring=" [OPTIONS]"
+ fi
+
+ if [[ -n "${UNIFFLE_SUBCMD_USAGE_COUNTER}"
+ && "${UNIFFLE_SUBCMD_USAGE_COUNTER}" -gt 0 ]]; then
+ havesubs=true
+ subcmdstring=" ${subcmdtext} [${subcmdtext} OPTIONS]"
+ fi
+
+ echo "Usage: ${cmd}${optstring}${subcmdstring}"
+ if [[ ${takesclass} = true ]]; then
+ echo " or ${cmd}${optstring} CLASSNAME [CLASSNAME OPTIONS]"
+ echo " where CLASSNAME is a user-provided Java class"
+ fi
+
+ if [[ "${haveoptions}" = true ]]; then
+ echo ""
+ echo " OPTIONS is none or any of:"
+ echo ""
+
+ uniffle_generic_columnprinter "" "${UNIFFLE_OPTION_USAGE[@]}"
+ fi
+
+ if [[ "${havesubs}" = true ]]; then
+ echo ""
+ echo " ${subcmdtext} is one of:"
+ echo ""
+
+ if [[ "${#UNIFFLE_SUBCMD_USAGE_TYPES[@]}" -gt 0 ]]; then
+ uniffle_sort_array UNIFFLE_SUBCMD_USAGE_TYPES
+ for subtype in "${UNIFFLE_SUBCMD_USAGE_TYPES[@]}"; do
+ cmdtype="$(tr '[:lower:]' '[:upper:]' <<< ${subtype:0:1})${subtype:1}"
+ printf "\n %s Commands:\n\n" "${cmdtype}"
+ uniffle_generic_columnprinter "${subtype}" "${UNIFFLE_SUBCMD_USAGE[@]}"
+ done
+ else
+ uniffle_generic_columnprinter "" "${UNIFFLE_SUBCMD_USAGE[@]}"
+ fi
+ echo ""
+ echo "${subcmdtext} may print help when invoked w/o parameters or with -h."
+ fi
+}
+
+#---
+# uniffle_generic_columnprinter: Print a screen-size aware two-column output,
+# if reqtype is not null, only print those requested.
+# @param1 reqtype
+# @param2 array
+#---
+function uniffle_generic_columnprinter
+{
+ declare reqtype=$1
+ shift
+ declare -a input=("$@")
+ declare -i i=0
+ declare -i counter=0
+ declare line
+ declare text
+ declare option
+ declare giventext
+ declare -i maxoptsize
+ declare -i foldsize
+ declare -a tmpa
+ declare numcols
+ declare brup
+
+ if [[ -n "${COLUMNS}" ]]; then
+ numcols=${COLUMNS}
+ else
+ numcols=$(tput cols) 2>/dev/null
+ COLUMNS=${numcols}
+ fi
+
+ if [[ -z "${numcols}"
+ || ! "${numcols}" =~ ^[0-9]+$ ]]; then
+ numcols=75
+ else
+ ((numcols=numcols-5))
+ fi
+
+ while read -r line; do
+ tmpa[${counter}]=${line}
+ ((counter=counter+1))
+ IFS='@' read -ra brup <<< "${line}"
+ option="${brup[0]}"
+ if [[ ${#option} -gt ${maxoptsize} ]]; then
+ maxoptsize=${#option}
+ fi
+ done < <(for text in "${input[@]}"; do
+ echo "${text}"
+ done | sort)
+
+ i=0
+ ((foldsize=numcols-maxoptsize))
+
+ until [[ $i -eq ${#tmpa[@]} ]]; do
+ IFS='@' read -ra brup <<< "${tmpa[$i]}"
+
+ option="${brup[0]}"
+ cmdtype="${brup[1]}"
+ giventext="${brup[2]}"
+
+ if [[ -n "${reqtype}" && "${cmdtype}" != "${reqtype}" ]]; then
+ ((i=i+1))
+ continue
+ fi
+
+ if [[ -z "${giventext}" ]]; then
+ giventext=${cmdtype}
+ fi
+
+ while read -r line; do
+ printf "%-${maxoptsize}s %-s\n" "${option}" "${line}"
+ option=" "
+ done < <(echo "${giventext}"| fold -s -w ${foldsize})
+ ((i=i+1))
+ done
+}
+
+#---
+# uniffle_add_subcommand: Add a subcommand to the usage output.
+# @param1 subcommand
+# @param2 subcommandtype
+# @param3 subcommanddesc
+#---
+function uniffle_add_subcommand
+{
+ declare subcmd=$1
+ declare subtype=$2
+ declare text=$3
+
+ uniffle_add_array_param UNIFFLE_SUBCMD_USAGE_TYPES "${subtype}"
+
+ # done in this order so that sort works later
+
UNIFFLE_SUBCMD_USAGE[${UNIFFLE_SUBCMD_USAGE_COUNTER}]="${subcmd}@${subtype}@${text}"
+ ((UNIFFLE_SUBCMD_USAGE_COUNTER=UNIFFLE_SUBCMD_USAGE_COUNTER+1))
+}
+
+#---
+# uniffle_add_option: Add an option to the usage output.
+# @param1 subcommand
+# @param2 subcommanddesc
+#---
+function uniffle_add_option
+{
+ local option=$1
+ local text=$2
+
+ UNIFFLE_OPTION_USAGE[${UNIFFLE_OPTION_USAGE}]="${option}@${text}"
+ ((UNIFFLE_OPTION_USAGE_COUNTER=UNIFFLE_OPTION_USAGE_COUNTER+1))
+}
+
+#---
+# uniffle_java_setup: Configure/verify ${JAVA_HOME}
+# return may exit on failure conditions
+#---
+function uniffle_java_setup
+{
+ # Bail if we did not detect it
+ if [[ -z "${JAVA_HOME}" ]]; then
+ uniffle_error "ERROR: JAVA_HOME is not set and could not be found."
+ exit 1
+ fi
+
+ if [[ ! -d "${JAVA_HOME}" ]]; then
+ uniffle_error "ERROR: JAVA_HOME ${JAVA_HOME} does not exist."
+ exit 1
+ fi
+
+ JAVA="${JAVA_HOME}/bin/java"
+
+ if [[ ! -x "$JAVA" ]]; then
+ uniffle_error "ERROR: $JAVA is not executable."
+ exit 1
+ fi
+}
+
+#---
+# uniffle_java_exec: Execute the Java `class`, passing along any `options`.
+# @param1 command
+# @param2 class
+#---
+function uniffle_java_exec
+{
+ # run a java command. this is used for
+ # non-daemons
+
+ local command=$1
+ local class=$2
+ shift 2
+
+ export CLASSPATH
+ exec "${JAVA}" "-Dproc_${command}" "${class}" "$@"
+}
+
+#---
+# uniffle_validate_classname: Verify that a shell command was passed a valid
class name.
+# @param1 command
+# @return 0 = success; 1 = failure w/user message;
+#---
+function uniffle_validate_classname
+{
+ local class=$1
+ shift 1
+
+ if [[ ! ${class} =~ \. ]]; then
+ # assuming the arg is typo of command if it does not conatain ".".
+ # class belonging to no package is not allowed as a result.
+ uniffle_error "ERROR: ${class} is not COMMAND nor fully qualified
CLASSNAME."
+ return 1
+ fi
+ return 0
+}
+
+function uniffle_generic_java_subcmd_handler
+{
+ uniffle_java_exec "${UNIFFLE_SUBCMD}" "${UNIFFLE_CLASSNAME}"
"${UNIFFLE_SUBCMD_ARGS[@]}"
+}
diff --git a/bin/utils.sh b/bin/utils.sh
index 6c4cadc5..55c9abea 100644
--- a/bin/utils.sh
+++ b/bin/utils.sh
@@ -21,6 +21,9 @@
set -o nounset # exit the script if you try to use an uninitialised variable
set -o errexit # exit the script if any statement returns a non-true return
value
+# By default, we enable verbose log printing.
+UNIFFLE_SHELL_SCRIPT_DEBUG=true
+
#---
# is_process_running: Checks if a process is running
# args: Process ID of running proccess
@@ -195,13 +198,17 @@ function load_rss_env {
JPS="${JAVA_HOME}/bin/jps"
# print
- echo "Using Java from ${JAVA_HOME}"
- echo "Using Hadoop from ${HADOOP_HOME}"
- echo "Using RSS from ${RSS_HOME}"
- echo "Using RSS conf from ${RSS_CONF_DIR}"
- echo "Using Hadoop conf from ${HADOOP_CONF_DIR}"
- echo "Write log file to ${RSS_LOG_DIR}"
- echo "Write pid file to ${RSS_PID_DIR}"
+ # If UNIFFLE_SHELL_SCRIPT_DEBUG is true, we will print JAVA_HOME,
HADOOP_HOME, RSS_HOME information etc.
+ # If UNIFFLE_SHELL_SCRIPT_DEBUG is false, we do not print Env information.
+ if [[ "${UNIFFLE_SHELL_SCRIPT_DEBUG}" = true ]]; then
+ echo "Using Java from ${JAVA_HOME}"
+ echo "Using Hadoop from ${HADOOP_HOME}"
+ echo "Using RSS from ${RSS_HOME}"
+ echo "Using RSS conf from ${RSS_CONF_DIR}"
+ echo "Using Hadoop conf from ${HADOOP_CONF_DIR}"
+ echo "Write log file to ${RSS_LOG_DIR}"
+ echo "Write pid file to ${RSS_PID_DIR}"
+ fi
set +o allexport
}
diff --git a/build_distribution.sh b/build_distribution.sh
index 7a853435..8c061e8d 100755
--- a/build_distribution.sh
+++ b/build_distribution.sh
@@ -154,6 +154,13 @@ echo "copy $COORDINATOR_JAR to ${COORDINATOR_JAR_DIR}"
cp $COORDINATOR_JAR ${COORDINATOR_JAR_DIR}
cp "${RSS_HOME}"/coordinator/target/jars/* ${COORDINATOR_JAR_DIR}
+CLI_JAR_DIR="${DISTDIR}/jars/cli"
+mkdir -p $CLI_JAR_DIR
+CLI_JAR="${RSS_HOME}/cli/target/cli-${VERSION}.jar"
+echo "copy $CLI_JAR to ${CLI_JAR_DIR}"
+cp $CLI_JAR ${CLI_JAR_DIR}
+cp "${RSS_HOME}"/cli/target/jars/* ${CLI_JAR_DIR}
+
CLIENT_JAR_DIR="${DISTDIR}/jars/client"
mkdir -p $CLIENT_JAR_DIR
diff --git a/cli/pom.xml b/cli/pom.xml
new file mode 100644
index 00000000..a8eaf2d9
--- /dev/null
+++ b/cli/pom.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+ ~ 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">
+ <parent>
+ <artifactId>uniffle-parent</artifactId>
+ <groupId>org.apache.uniffle</groupId>
+ <version>0.8.0-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>cli</artifactId>
+ <packaging>jar</packaging>
+ <name>Apache Uniffle CLI</name>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.uniffle</groupId>
+ <artifactId>rss-common</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>commons-cli</groupId>
+ <artifactId>commons-cli</artifactId>
+ </dependency>
+ </dependencies>
+</project>
diff --git
a/cli/src/main/java/org/apache/uniffle/AbstractCustomCommandLine.java
b/cli/src/main/java/org/apache/uniffle/AbstractCustomCommandLine.java
new file mode 100644
index 00000000..cb97ccf6
--- /dev/null
+++ b/cli/src/main/java/org/apache/uniffle/AbstractCustomCommandLine.java
@@ -0,0 +1,81 @@
+/*
+ * 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.uniffle;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.slf4j.Logger;
+
+public abstract class AbstractCustomCommandLine implements CustomCommandLine {
+
+ protected void printUsage() {
+ System.out.println("Usage:");
+ HelpFormatter formatter = new HelpFormatter();
+ formatter.setWidth(200);
+ formatter.setLeftPadding(5);
+
+ formatter.setSyntaxPrefix(" Optional");
+ Options options = new Options();
+ addGeneralOptions(options);
+ addRunOptions(options);
+ formatter.printHelp(" ", options);
+ }
+
+ public static int handleCliArgsException(UniffleCliArgsException e, Logger
logger) {
+ logger.error("Could not parse the command line arguments.", e);
+
+ System.out.println(e.getMessage());
+ System.out.println();
+ System.out.println("Use the help option (-h or --help) to get help on the
command.");
+ return 1;
+ }
+
+ public static int handleError(Throwable t, Logger logger) {
+ logger.error("Error while running the Uniffle Command.", t);
+
+ System.err.println();
+
System.err.println("------------------------------------------------------------");
+ System.err.println(" The program finished with the following exception:");
+ System.err.println();
+
+ t.printStackTrace();
+ return 1;
+ }
+
+ public static CommandLine parse(Options options, String[] args, boolean
stopAtNonOptions)
+ throws UniffleCliArgsException {
+ final DefaultParser parser = new DefaultParser();
+
+ try {
+ return parser.parse(options, args, stopAtNonOptions);
+ } catch (ParseException e) {
+ throw new UniffleCliArgsException(e.getMessage());
+ }
+ }
+
+ public CommandLine parseCommandLineOptions(String[] args, boolean
stopAtNonOptions)
+ throws UniffleCliArgsException {
+ final Options options = new Options();
+ addGeneralOptions(options);
+ addRunOptions(options);
+ return parse(options, args, stopAtNonOptions);
+ }
+}
diff --git a/cli/src/main/java/org/apache/uniffle/CustomCommandLine.java
b/cli/src/main/java/org/apache/uniffle/CustomCommandLine.java
new file mode 100644
index 00000000..8b0411e0
--- /dev/null
+++ b/cli/src/main/java/org/apache/uniffle/CustomCommandLine.java
@@ -0,0 +1,27 @@
+/*
+ * 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.uniffle;
+
+import org.apache.commons.cli.Options;
+
+public interface CustomCommandLine {
+
+ void addRunOptions(Options baseOptions);
+
+ void addGeneralOptions(Options baseOptions);
+}
diff --git a/cli/src/main/java/org/apache/uniffle/UniffleCliArgsException.java
b/cli/src/main/java/org/apache/uniffle/UniffleCliArgsException.java
new file mode 100644
index 00000000..edc1d643
--- /dev/null
+++ b/cli/src/main/java/org/apache/uniffle/UniffleCliArgsException.java
@@ -0,0 +1,30 @@
+/*
+ * 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.uniffle;
+
+public class UniffleCliArgsException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public UniffleCliArgsException(String message) {
+ super(message);
+ }
+
+ public UniffleCliArgsException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/cli/src/main/java/org/apache/uniffle/cli/UniffleCLI.java
b/cli/src/main/java/org/apache/uniffle/cli/UniffleCLI.java
new file mode 100644
index 00000000..6e7631ac
--- /dev/null
+++ b/cli/src/main/java/org/apache/uniffle/cli/UniffleCLI.java
@@ -0,0 +1,96 @@
+/*
+ * 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.uniffle.cli;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.uniffle.AbstractCustomCommandLine;
+import org.apache.uniffle.UniffleCliArgsException;
+
+public class UniffleCLI extends AbstractCustomCommandLine {
+
+ private static final Logger LOG = LoggerFactory.getLogger(UniffleCLI.class);
+ private final Options allOptions;
+ private final Option uniffleClientCli;
+ private final Option uniffleAdminCli;
+ private final Option help;
+
+ public UniffleCLI(String shortPrefix, String longPrefix) {
+ allOptions = new Options();
+ uniffleClientCli = new Option(shortPrefix + "c", longPrefix + "cli",
+ true, "This is an client cli command that will print args.");
+ uniffleAdminCli = new Option(shortPrefix + "a", longPrefix + "admin",
+ true, "This is an admin command that will print args.");
+ help = new Option(shortPrefix + "h", longPrefix + "help",
+ false, "Help for the Uniffle CLI.");
+ allOptions.addOption(uniffleClientCli);
+ allOptions.addOption(uniffleAdminCli);
+ allOptions.addOption(help);
+ }
+
+ public int run(String[] args) throws UniffleCliArgsException {
+ final CommandLine cmd = parseCommandLineOptions(args, true);
+
+ if (cmd.hasOption(help.getOpt())) {
+ printUsage();
+ return 0;
+ }
+
+ if (cmd.hasOption(uniffleClientCli.getOpt())) {
+ String cliArgs = cmd.getOptionValue(uniffleClientCli.getOpt());
+ System.out.println("uniffle-client-cli : " + cliArgs);
+ return 0;
+ }
+
+ if (cmd.hasOption(uniffleAdminCli.getOpt())) {
+ String cliArgs = cmd.getOptionValue(uniffleAdminCli.getOpt());
+ System.out.println("uniffle-admin-cli : " + cliArgs);
+ return 0;
+ }
+
+ return 1;
+ }
+
+ @Override
+ public void addRunOptions(Options baseOptions) {
+ baseOptions.addOption(uniffleClientCli);
+ baseOptions.addOption(uniffleAdminCli);
+ }
+
+ @Override
+ public void addGeneralOptions(Options baseOptions) {
+ baseOptions.addOption(help);
+ }
+
+ public static void main(String[] args) {
+ int retCode;
+ try {
+ final UniffleCLI cli = new UniffleCLI("", "");
+ retCode = cli.run(args);
+ } catch (UniffleCliArgsException e) {
+ retCode = AbstractCustomCommandLine.handleCliArgsException(e, LOG);
+ } catch (Exception e) {
+ retCode = AbstractCustomCommandLine.handleError(e, LOG);
+ }
+ System.exit(retCode);
+ }
+}
diff --git a/cli/src/test/java/org/apache/uniffle/cli/UniffleTestCLI.java
b/cli/src/test/java/org/apache/uniffle/cli/UniffleTestCLI.java
new file mode 100644
index 00000000..207538ab
--- /dev/null
+++ b/cli/src/test/java/org/apache/uniffle/cli/UniffleTestCLI.java
@@ -0,0 +1,86 @@
+/*
+ * 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.uniffle.cli;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.PrintStream;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.apache.uniffle.UniffleCliArgsException;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class UniffleTestCLI {
+
+ private UniffleCLI uniffleCLI;
+
+ @BeforeEach
+ public void setup() throws Exception {
+ uniffleCLI = new UniffleCLI("", "");
+ }
+
+ @Test
+ public void testHelp() throws UniffleCliArgsException, IOException {
+ final PrintStream oldOutPrintStream = System.out;
+ final PrintStream oldErrPrintStream = System.err;
+ ByteArrayOutputStream dataOut = new ByteArrayOutputStream();
+ ByteArrayOutputStream dataErr = new ByteArrayOutputStream();
+ System.setOut(new PrintStream(dataOut));
+ System.setErr(new PrintStream(dataErr));
+
+ String[] args1 = {"-help"};
+ assertEquals(0, uniffleCLI.run(args1));
+ oldOutPrintStream.println(dataOut);
+ assertTrue(dataOut.toString().contains(
+ "-a,--admin <arg> This is an admin command that will print args."));
+ assertTrue(dataOut.toString().contains(
+ "-c,--cli <arg> This is an client cli command that will print
args."));
+ assertTrue(dataOut.toString().contains(
+ "-h,--help Help for the Uniffle CLI."));
+
+ System.setOut(oldOutPrintStream);
+ System.setErr(oldErrPrintStream);
+
+ dataOut.close();
+ dataErr.close();
+ }
+
+ @Test
+ public void testExampleCLI() throws UniffleCliArgsException, IOException {
+ final PrintStream oldOutPrintStream = System.out;
+ final PrintStream oldErrPrintStream = System.err;
+ ByteArrayOutputStream dataOut = new ByteArrayOutputStream();
+ ByteArrayOutputStream dataErr = new ByteArrayOutputStream();
+ System.setOut(new PrintStream(dataOut));
+ System.setErr(new PrintStream(dataErr));
+
+ String[] args = {"-c","hello world"};
+ assertEquals(0, uniffleCLI.run(args));
+ oldOutPrintStream.println(dataOut);
+ assertTrue(dataOut.toString().contains("uniffle-client-cli : hello
world"));
+ System.setOut(oldOutPrintStream);
+ System.setErr(oldErrPrintStream);
+
+ dataOut.close();
+ dataErr.close();
+ }
+}
diff --git a/pom.xml b/pom.xml
index 503d7d6b..0368f717 100644
--- a/pom.xml
+++ b/pom.xml
@@ -118,6 +118,7 @@
<module>server</module>
<module>client</module>
<module>integration-test/common</module>
+ <module>cli</module>
</modules>
<dependencies>
@@ -641,6 +642,13 @@
<type>test-jar</type>
<scope>test</scope>
</dependency>
+
+ <dependency>
+ <groupId>commons-cli</groupId>
+ <artifactId>commons-cli</artifactId>
+ <version>1.5.0</version>
+ </dependency>
+
</dependencies>
</dependencyManagement>