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>
 

Reply via email to