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

jshao pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/main by this push:
     new 626bc7943 [#5506] feat(CLI): Table formatted output (#5714)
626bc7943 is described below

commit 626bc7943b56af431215ea3cd4aae9ce5cee6bef
Author: Jimmy Lee <55496001+wau...@users.noreply.github.com>
AuthorDate: Mon Dec 2 10:52:03 2024 +0800

    [#5506] feat(CLI): Table formatted output (#5714)
    
    ### What changes were proposed in this pull request?
    
    Enhance the Gravitino CLI with formatted output.
    
    <img width="323" alt="383987880-78e48033-601e-422f-b5bb-20061536c6b2"
    
src="https://github.com/user-attachments/assets/9655c081-1575-44c3-bcce-49ca2b49a259";>
    
    ### Why are the changes needed?
    
    Issue: #5506
    Cause the original PR (https://github.com/apache/gravitino/pull/5606)
    cannot be reopened, I create a new PR.
    The purpose is to make Gravitino CLI output look more readable;
    currently, it supports only table and plain output formats.
    
    ### Does this PR introduce _any_ user-facing change?
    
    No.
    
    ### How was this patch tested?
    
    ```shell
    gcli metalake list
    gcli metalake list --output plain
    gcli metalake list --output table
    ```
---
 clients/cli/build.gradle.kts                       |  11 +-
 .../apache/gravitino/cli/GravitinoCommandLine.java |   8 +-
 .../org/apache/gravitino/cli/GravitinoOptions.java |   3 +
 .../apache/gravitino/cli/TestableCommandLine.java  |  13 +-
 .../gravitino/cli/commands/CatalogDetails.java     |  15 +-
 .../org/apache/gravitino/cli/commands/Command.java |  30 ++++
 .../gravitino/cli/commands/ListMetalakes.java      |  21 +--
 .../gravitino/cli/commands/MetalakeDetails.java    |  14 +-
 .../apache/gravitino/cli/outputs/OutputFormat.java |  24 +++
 .../apache/gravitino/cli/outputs/PlainFormat.java  |  71 +++++++++
 .../apache/gravitino/cli/outputs/TableFormat.java  | 157 +++++++++++++++++++
 .../apache/gravitino/cli/TestCatalogCommands.java  |   3 +-
 .../apache/gravitino/cli/TestMetalakeCommands.java |   6 +-
 .../cli/integration/test/TableFormatOutputIT.java  | 166 +++++++++++++++++++++
 14 files changed, 495 insertions(+), 47 deletions(-)

diff --git a/clients/cli/build.gradle.kts b/clients/cli/build.gradle.kts
index 9ce570ef8..ae45fc968 100644
--- a/clients/cli/build.gradle.kts
+++ b/clients/cli/build.gradle.kts
@@ -34,7 +34,16 @@ dependencies {
   testImplementation(libs.junit.jupiter.api)
   testImplementation(libs.junit.jupiter.params)
   testImplementation(libs.mockito.core)
-
+  testImplementation(project(":core")) {
+    exclude("org.apache.logging.log4j")
+  }
+  testImplementation(project(":server")) {
+    exclude("org.apache.logging.log4j")
+  }
+  testImplementation(project(":server-common")) {
+    exclude("org.apache.logging.log4j")
+  }
+  testImplementation(project(":integration-test-common", "testArtifacts"))
   testRuntimeOnly(libs.junit.jupiter.engine)
 }
 
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/GravitinoCommandLine.java 
b/clients/cli/src/main/java/org/apache/gravitino/cli/GravitinoCommandLine.java
index ca149672a..e7a36d016 100644
--- 
a/clients/cli/src/main/java/org/apache/gravitino/cli/GravitinoCommandLine.java
+++ 
b/clients/cli/src/main/java/org/apache/gravitino/cli/GravitinoCommandLine.java
@@ -153,15 +153,16 @@ public class GravitinoCommandLine extends 
TestableCommandLine {
     String url = getUrl();
     FullName name = new FullName(line);
     String metalake = name.getMetalakeName();
+    String outputFormat = line.getOptionValue(GravitinoOptions.OUTPUT);
 
     if (CommandActions.DETAILS.equals(command)) {
       if (line.hasOption(GravitinoOptions.AUDIT)) {
         newMetalakeAudit(url, ignore, metalake).handle();
       } else {
-        newMetalakeDetails(url, ignore, metalake).handle();
+        newMetalakeDetails(url, ignore, metalake, outputFormat).handle();
       }
     } else if (CommandActions.LIST.equals(command)) {
-      newListMetalakes(url, ignore).handle();
+      newListMetalakes(url, ignore, outputFormat).handle();
     } else if (CommandActions.CREATE.equals(command)) {
       String comment = line.getOptionValue(GravitinoOptions.COMMENT);
       newCreateMetalake(url, ignore, metalake, comment).handle();
@@ -197,6 +198,7 @@ public class GravitinoCommandLine extends 
TestableCommandLine {
     String url = getUrl();
     FullName name = new FullName(line);
     String metalake = name.getMetalakeName();
+    String outputFormat = line.getOptionValue(GravitinoOptions.OUTPUT);
 
     if (CommandActions.LIST.equals(command)) {
       newListCatalogs(url, ignore, metalake).handle();
@@ -209,7 +211,7 @@ public class GravitinoCommandLine extends 
TestableCommandLine {
       if (line.hasOption(GravitinoOptions.AUDIT)) {
         newCatalogAudit(url, ignore, metalake, catalog).handle();
       } else {
-        newCatalogDetails(url, ignore, metalake, catalog).handle();
+        newCatalogDetails(url, ignore, metalake, catalog, 
outputFormat).handle();
       }
     } else if (CommandActions.CREATE.equals(command)) {
       String comment = line.getOptionValue(GravitinoOptions.COMMENT);
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/GravitinoOptions.java 
b/clients/cli/src/main/java/org/apache/gravitino/cli/GravitinoOptions.java
index 863bc5d21..8cf5f0cc1 100644
--- a/clients/cli/src/main/java/org/apache/gravitino/cli/GravitinoOptions.java
+++ b/clients/cli/src/main/java/org/apache/gravitino/cli/GravitinoOptions.java
@@ -47,6 +47,7 @@ public class GravitinoOptions {
   public static final String INDEX = "index";
   public static final String DISTRIBUTION = "distribution";
   public static final String PARTITION = "partition";
+  public static final String OUTPUT = "output";
 
   /**
    * Builds and returns the CLI options for Gravitino.
@@ -90,6 +91,8 @@ public class GravitinoOptions {
     // Force delete entities and rename metalake operations
     options.addOption(createSimpleOption("f", FORCE, "force operation"));
 
+    options.addOption(createArgOption(null, OUTPUT, "output format 
(plain/table)"));
+
     return options;
   }
 
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/TestableCommandLine.java 
b/clients/cli/src/main/java/org/apache/gravitino/cli/TestableCommandLine.java
index 248f57cd6..57de13f0a 100644
--- 
a/clients/cli/src/main/java/org/apache/gravitino/cli/TestableCommandLine.java
+++ 
b/clients/cli/src/main/java/org/apache/gravitino/cli/TestableCommandLine.java
@@ -129,12 +129,13 @@ public class TestableCommandLine {
     return new MetalakeAudit(url, ignore, metalake);
   }
 
-  protected MetalakeDetails newMetalakeDetails(String url, boolean ignore, 
String metalake) {
-    return new MetalakeDetails(url, ignore, metalake);
+  protected MetalakeDetails newMetalakeDetails(
+      String url, boolean ignore, String metalake, String outputFormat) {
+    return new MetalakeDetails(url, ignore, metalake, outputFormat);
   }
 
-  protected ListMetalakes newListMetalakes(String url, boolean ignore) {
-    return new ListMetalakes(url, ignore);
+  protected ListMetalakes newListMetalakes(String url, boolean ignore, String 
outputFormat) {
+    return new ListMetalakes(url, ignore, outputFormat);
   }
 
   protected CreateMetalake newCreateMetalake(
@@ -178,8 +179,8 @@ public class TestableCommandLine {
   }
 
   protected CatalogDetails newCatalogDetails(
-      String url, boolean ignore, String metalake, String catalog) {
-    return new CatalogDetails(url, ignore, metalake, catalog);
+      String url, boolean ignore, String metalake, String catalog, String 
outputFormat) {
+    return new CatalogDetails(url, ignore, metalake, catalog, outputFormat);
   }
 
   protected ListCatalogs newListCatalogs(String url, boolean ignore, String 
metalake) {
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/CatalogDetails.java
 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/CatalogDetails.java
index 41451daac..bebe536fa 100644
--- 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/CatalogDetails.java
+++ 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/CatalogDetails.java
@@ -37,9 +37,11 @@ public class CatalogDetails extends Command {
    * @param ignoreVersions If true don't check the client/server versions 
match.
    * @param metalake The name of the metalake.
    * @param catalog The name of the catalog.
+   * @param outputFormat The output format.
    */
-  public CatalogDetails(String url, boolean ignoreVersions, String metalake, 
String catalog) {
-    super(url, ignoreVersions);
+  public CatalogDetails(
+      String url, boolean ignoreVersions, String metalake, String catalog, 
String outputFormat) {
+    super(url, ignoreVersions, outputFormat);
     this.metalake = metalake;
     this.catalog = catalog;
   }
@@ -52,20 +54,13 @@ public class CatalogDetails extends Command {
     try {
       GravitinoClient client = buildClient(metalake);
       result = client.loadCatalog(catalog);
+      output(result);
     } catch (NoSuchMetalakeException err) {
       System.err.println(ErrorMessages.UNKNOWN_METALAKE);
-      return;
     } catch (NoSuchCatalogException err) {
       System.err.println(ErrorMessages.UNKNOWN_CATALOG);
-      return;
     } catch (Exception exp) {
       System.err.println(exp.getMessage());
-      return;
-    }
-
-    if (result != null) {
-      System.out.println(
-          result.name() + "," + result.type() + "," + result.provider() + "," 
+ result.comment());
     }
   }
 }
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/Command.java 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/Command.java
index 040bd7141..1753e5743 100644
--- a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/Command.java
+++ b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/Command.java
@@ -19,6 +19,8 @@
 
 package org.apache.gravitino.cli.commands;
 
+import org.apache.gravitino.cli.outputs.PlainFormat;
+import org.apache.gravitino.cli.outputs.TableFormat;
 import org.apache.gravitino.client.GravitinoAdminClient;
 import org.apache.gravitino.client.GravitinoClient;
 import org.apache.gravitino.exceptions.NoSuchMetalakeException;
@@ -27,6 +29,9 @@ import 
org.apache.gravitino.exceptions.NoSuchMetalakeException;
 public abstract class Command {
   private final String url;
   private final boolean ignoreVersions;
+  private final String outputFormat;
+  public static String OUTPUT_FORMAT_TABLE = "table";
+  public static String OUTPUT_FORMAT_PLAIN = "plain";
 
   /**
    * Command constructor.
@@ -35,8 +40,13 @@ public abstract class Command {
    * @param ignoreVersions If true don't check the client/server versions 
match.
    */
   public Command(String url, boolean ignoreVersions) {
+    this(url, ignoreVersions, null);
+  }
+
+  public Command(String url, boolean ignoreVersions, String outputFormat) {
     this.url = url;
     this.ignoreVersions = ignoreVersions;
+    this.outputFormat = outputFormat;
   }
 
   /** All commands have a handle method to handle and run the required 
command. */
@@ -69,4 +79,24 @@ public abstract class Command {
       return GravitinoAdminClient.builder(url).build();
     }
   }
+
+  /**
+   * Outputs the entity to the console.
+   *
+   * @param entity The entity to output.
+   */
+  protected <T> void output(T entity) {
+    if (outputFormat == null) {
+      PlainFormat.output(entity);
+      return;
+    }
+
+    if (outputFormat.equals(OUTPUT_FORMAT_TABLE)) {
+      TableFormat.output(entity);
+    } else if (outputFormat.equals(OUTPUT_FORMAT_PLAIN)) {
+      PlainFormat.output(entity);
+    } else {
+      throw new IllegalArgumentException("Unsupported output format");
+    }
+  }
 }
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListMetalakes.java
 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListMetalakes.java
index 26668157a..de7571568 100644
--- 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListMetalakes.java
+++ 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListMetalakes.java
@@ -19,9 +19,6 @@
 
 package org.apache.gravitino.cli.commands;
 
-import com.google.common.base.Joiner;
-import java.util.ArrayList;
-import java.util.List;
 import org.apache.gravitino.Metalake;
 import org.apache.gravitino.client.GravitinoAdminClient;
 
@@ -33,30 +30,22 @@ public class ListMetalakes extends Command {
    *
    * @param url The URL of the Gravitino server.
    * @param ignoreVersions If true don't check the client/server versions 
match.
+   * @param outputFormat The output format.
    */
-  public ListMetalakes(String url, boolean ignoreVersions) {
-    super(url, ignoreVersions);
+  public ListMetalakes(String url, boolean ignoreVersions, String 
outputFormat) {
+    super(url, ignoreVersions, outputFormat);
   }
 
   /** Lists all metalakes. */
   @Override
   public void handle() {
-    Metalake[] metalakes = new Metalake[0];
+    Metalake[] metalakes;
     try {
       GravitinoAdminClient client = buildAdminClient();
       metalakes = client.listMetalakes();
+      output(metalakes);
     } catch (Exception exp) {
       System.err.println(exp.getMessage());
-      return;
     }
-
-    List<String> metalakeNames = new ArrayList<>();
-    for (int i = 0; i < metalakes.length; i++) {
-      metalakeNames.add(metalakes[i].name());
-    }
-
-    String all = Joiner.on(System.lineSeparator()).join(metalakeNames);
-
-    System.out.println(all.toString());
   }
 }
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/MetalakeDetails.java
 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/MetalakeDetails.java
index c00c90c53..127b9a584 100644
--- 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/MetalakeDetails.java
+++ 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/MetalakeDetails.java
@@ -19,6 +19,7 @@
 
 package org.apache.gravitino.cli.commands;
 
+import org.apache.gravitino.Metalake;
 import org.apache.gravitino.cli.ErrorMessages;
 import org.apache.gravitino.client.GravitinoClient;
 import org.apache.gravitino.exceptions.NoSuchMetalakeException;
@@ -33,27 +34,24 @@ public class MetalakeDetails extends Command {
    * @param url The URL of the Gravitino server.
    * @param ignoreVersions If true don't check the client/server versions 
match.
    * @param metalake The name of the metalake.
+   * @param outputFormat The output format.
    */
-  public MetalakeDetails(String url, boolean ignoreVersions, String metalake) {
-    super(url, ignoreVersions);
+  public MetalakeDetails(String url, boolean ignoreVersions, String metalake, 
String outputFormat) {
+    super(url, ignoreVersions, outputFormat);
     this.metalake = metalake;
   }
 
   /** Displays the name and comment of a metalake. */
   @Override
   public void handle() {
-    String comment = "";
     try {
       GravitinoClient client = buildClient(metalake);
-      comment = client.loadMetalake(metalake).comment();
+      Metalake metalakeEntity = client.loadMetalake(metalake);
+      output(metalakeEntity);
     } catch (NoSuchMetalakeException err) {
       System.err.println(ErrorMessages.UNKNOWN_METALAKE);
-      return;
     } catch (Exception exp) {
       System.err.println(exp.getMessage());
-      return;
     }
-
-    System.out.println(metalake + "," + comment);
   }
 }
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/OutputFormat.java 
b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/OutputFormat.java
new file mode 100644
index 000000000..8e6ab3116
--- /dev/null
+++ 
b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/OutputFormat.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.
+ */
+package org.apache.gravitino.cli.outputs;
+
+/** Output format interface for the CLI results. */
+public interface OutputFormat<T> {
+  void output(T object);
+}
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/PlainFormat.java 
b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/PlainFormat.java
new file mode 100644
index 000000000..4674d3f88
--- /dev/null
+++ 
b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/PlainFormat.java
@@ -0,0 +1,71 @@
+/*
+ * 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.gravitino.cli.outputs;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.Metalake;
+
+/** Plain format to print a pretty string to standard out. */
+public class PlainFormat {
+  public static void output(Object object) {
+    if (object instanceof Metalake) {
+      new MetalakeStringFormat().output((Metalake) object);
+    } else if (object instanceof Metalake[]) {
+      new MetalakesStringFormat().output((Metalake[]) object);
+    } else if (object instanceof Catalog) {
+      new CatalogStringFormat().output((Catalog) object);
+    } else {
+      throw new IllegalArgumentException("Unsupported object type");
+    }
+  }
+
+  static final class MetalakeStringFormat implements OutputFormat<Metalake> {
+    @Override
+    public void output(Metalake metalake) {
+      System.out.println(metalake.name() + "," + metalake.comment());
+    }
+  }
+
+  static final class MetalakesStringFormat implements OutputFormat<Metalake[]> 
{
+    @Override
+    public void output(Metalake[] metalakes) {
+      List<String> metalakeNames =
+          
Arrays.stream(metalakes).map(Metalake::name).collect(Collectors.toList());
+      String all = String.join(System.lineSeparator(), metalakeNames);
+      System.out.println(all);
+    }
+  }
+
+  static final class CatalogStringFormat implements OutputFormat<Catalog> {
+    @Override
+    public void output(Catalog catalog) {
+      System.out.println(
+          catalog.name()
+              + ","
+              + catalog.type()
+              + ","
+              + catalog.provider()
+              + ","
+              + catalog.comment());
+    }
+  }
+}
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/TableFormat.java 
b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/TableFormat.java
new file mode 100644
index 000000000..a1975b9f3
--- /dev/null
+++ 
b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/TableFormat.java
@@ -0,0 +1,157 @@
+/*
+ * 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.gravitino.cli.outputs;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.Metalake;
+
+/** Table format to print a pretty table to standard out. */
+public class TableFormat {
+  public static void output(Object object) {
+    if (object instanceof Metalake) {
+      new MetalakeTableFormat().output((Metalake) object);
+    } else if (object instanceof Metalake[]) {
+      new MetalakesTableFormat().output((Metalake[]) object);
+    } else if (object instanceof Catalog) {
+      new CatalogTableFormat().output((Catalog) object);
+    } else {
+      throw new IllegalArgumentException("Unsupported object type");
+    }
+  }
+
+  static final class MetalakeTableFormat implements OutputFormat<Metalake> {
+    @Override
+    public void output(Metalake metalake) {
+      List<String> headers = Arrays.asList("metalake", "comment");
+      List<List<String>> rows = new ArrayList<>();
+      rows.add(Arrays.asList(metalake.name(), metalake.comment()));
+      TableFormatImpl tableFormat = new TableFormatImpl();
+      tableFormat.print(headers, rows);
+    }
+  }
+
+  static final class MetalakesTableFormat implements OutputFormat<Metalake[]> {
+    @Override
+    public void output(Metalake[] metalakes) {
+      List<String> headers = Collections.singletonList("metalake");
+      List<List<String>> rows = new ArrayList<>();
+      for (int i = 0; i < metalakes.length; i++) {
+        rows.add(Arrays.asList(metalakes[i].name()));
+      }
+      TableFormatImpl tableFormat = new TableFormatImpl();
+      tableFormat.print(headers, rows);
+    }
+  }
+
+  static final class CatalogTableFormat implements OutputFormat<Catalog> {
+    @Override
+    public void output(Catalog catalog) {
+      List<String> headers = Arrays.asList("catalog", "type", "provider", 
"comment");
+      List<List<String>> rows = new ArrayList<>();
+      rows.add(
+          Arrays.asList(
+              catalog.name(),
+              catalog.type().toString(),
+              catalog.provider(),
+              catalog.comment() + ""));
+      TableFormatImpl tableFormat = new TableFormatImpl();
+      tableFormat.print(headers, rows);
+    }
+  }
+
+  static final class TableFormatImpl {
+    private int[] maxElementLengths;
+    private final String horizontalDelimiter = "-";
+    private final String verticalDelimiter = "|";
+    private final String crossDelimiter = "+";
+    private final String indent = " ";
+
+    public void debug() {
+      System.out.println();
+      Arrays.stream(maxElementLengths).forEach(e -> System.out.print(e + " "));
+    }
+
+    public void print(List<String> headers, List<List<String>> rows) {
+      if (rows.size() > 0 && headers.size() != rows.get(0).size()) {
+        throw new IllegalArgumentException("Number of columns is not equal.");
+      }
+      maxElementLengths = new int[headers.size()];
+      updateMaxLengthsFromList(headers);
+      updateMaxLengthsFromNestedList(rows);
+
+      // print headers
+      printLine();
+      System.out.println();
+      for (int i = 0; i < headers.size(); ++i) {
+        System.out.printf(
+            verticalDelimiter + indent + "%-" + maxElementLengths[i] + "s" + 
indent,
+            headers.get(i));
+      }
+      System.out.println(verticalDelimiter);
+      printLine();
+      System.out.println();
+
+      // print rows
+      for (int i = 0; i < rows.size(); ++i) {
+        List<String> columns = rows.get(i);
+        for (int j = 0; j < columns.size(); ++j) {
+          System.out.printf(
+              verticalDelimiter + indent + "%-" + maxElementLengths[j] + "s" + 
indent,
+              columns.get(j));
+        }
+        System.out.println(verticalDelimiter);
+      }
+      printLine();
+      // add one more line
+      System.out.println("");
+    }
+
+    private void updateMaxLengthsFromList(List<String> elements) {
+      String s;
+      for (int i = 0; i < elements.size(); ++i) {
+        s = elements.get(i);
+        if (s.length() > maxElementLengths[i]) maxElementLengths[i] = 
s.length();
+      }
+    }
+
+    private void updateMaxLengthsFromNestedList(List<List<String>> elements) {
+      for (List<String> row : elements) {
+        String s;
+        for (int i = 0; i < row.size(); ++i) {
+          s = row.get(i);
+          if (s.length() > maxElementLengths[i]) maxElementLengths[i] = 
s.length();
+        }
+      }
+    }
+
+    private void printLine() {
+      System.out.print(crossDelimiter);
+      for (int i = 0; i < maxElementLengths.length; ++i) {
+        for (int j = 0; j < maxElementLengths[i] + indent.length() * 2; ++j) {
+          System.out.print(horizontalDelimiter);
+        }
+        System.out.print(crossDelimiter);
+      }
+    }
+  }
+}
diff --git 
a/clients/cli/src/test/java/org/apache/gravitino/cli/TestCatalogCommands.java 
b/clients/cli/src/test/java/org/apache/gravitino/cli/TestCatalogCommands.java
index c7a24b7a3..c337a342c 100644
--- 
a/clients/cli/src/test/java/org/apache/gravitino/cli/TestCatalogCommands.java
+++ 
b/clients/cli/src/test/java/org/apache/gravitino/cli/TestCatalogCommands.java
@@ -81,7 +81,8 @@ class TestCatalogCommands {
                 mockCommandLine, mockOptions, CommandEntities.CATALOG, 
CommandActions.DETAILS));
     doReturn(mockDetails)
         .when(commandLine)
-        .newCatalogDetails(GravitinoCommandLine.DEFAULT_URL, false, 
"metalake_demo", "catalog");
+        .newCatalogDetails(
+            GravitinoCommandLine.DEFAULT_URL, false, "metalake_demo", 
"catalog", null);
     commandLine.handleCommandLine();
     verify(mockDetails).handle();
   }
diff --git 
a/clients/cli/src/test/java/org/apache/gravitino/cli/TestMetalakeCommands.java 
b/clients/cli/src/test/java/org/apache/gravitino/cli/TestMetalakeCommands.java
index d602d56d9..2b94a80a9 100644
--- 
a/clients/cli/src/test/java/org/apache/gravitino/cli/TestMetalakeCommands.java
+++ 
b/clients/cli/src/test/java/org/apache/gravitino/cli/TestMetalakeCommands.java
@@ -59,7 +59,9 @@ class TestMetalakeCommands {
         spy(
             new GravitinoCommandLine(
                 mockCommandLine, mockOptions, CommandEntities.METALAKE, 
CommandActions.LIST));
-    
doReturn(mockList).when(commandLine).newListMetalakes(GravitinoCommandLine.DEFAULT_URL,
 false);
+    doReturn(mockList)
+        .when(commandLine)
+        .newListMetalakes(GravitinoCommandLine.DEFAULT_URL, false, null);
     commandLine.handleCommandLine();
     verify(mockList).handle();
   }
@@ -76,7 +78,7 @@ class TestMetalakeCommands {
                 mockCommandLine, mockOptions, CommandEntities.METALAKE, 
CommandActions.DETAILS));
     doReturn(mockDetails)
         .when(commandLine)
-        .newMetalakeDetails(GravitinoCommandLine.DEFAULT_URL, false, 
"metalake_demo");
+        .newMetalakeDetails(GravitinoCommandLine.DEFAULT_URL, false, 
"metalake_demo", null);
     commandLine.handleCommandLine();
     verify(mockDetails).handle();
   }
diff --git 
a/clients/cli/src/test/java/org/apache/gravitino/cli/integration/test/TableFormatOutputIT.java
 
b/clients/cli/src/test/java/org/apache/gravitino/cli/integration/test/TableFormatOutputIT.java
new file mode 100644
index 000000000..55cab8c5b
--- /dev/null
+++ 
b/clients/cli/src/test/java/org/apache/gravitino/cli/integration/test/TableFormatOutputIT.java
@@ -0,0 +1,166 @@
+/*
+ * 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.gravitino.cli.integration.test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.nio.charset.StandardCharsets;
+import org.apache.gravitino.cli.GravitinoOptions;
+import org.apache.gravitino.cli.Main;
+import org.apache.gravitino.integration.test.util.BaseIT;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+public class TableFormatOutputIT extends BaseIT {
+  private String gravitinoUrl;
+
+  @BeforeAll
+  public void startUp() {
+    gravitinoUrl = String.format("http://127.0.0.1:%d";, 
getGravitinoServerPort());
+    String[] create_metalake_args = {
+      "metalake",
+      "create",
+      commandArg(GravitinoOptions.METALAKE),
+      "my_metalake",
+      commandArg(GravitinoOptions.COMMENT),
+      "my metalake",
+      commandArg(GravitinoOptions.URL),
+      gravitinoUrl
+    };
+    Main.main(create_metalake_args);
+
+    String[] create_catalog_args = {
+      "catalog",
+      "create",
+      commandArg(GravitinoOptions.METALAKE),
+      "my_metalake",
+      commandArg(GravitinoOptions.NAME),
+      "postgres",
+      commandArg(GravitinoOptions.PROVIDER),
+      "postgres",
+      commandArg(GravitinoOptions.PROPERTIES),
+      
"jdbc-url=jdbc:postgresql://postgresql-host/mydb,jdbc-user=user,jdbc-password=password,jdbc-database=db,jdbc-driver=org.postgresql.Driver",
+      commandArg(GravitinoOptions.URL),
+      gravitinoUrl
+    };
+    Main.main(create_catalog_args);
+  }
+
+  @Test
+  public void testMetalakeListCommand() {
+    // Create a byte array output stream to capture the output of the command
+    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    PrintStream originalOut = System.out;
+    System.setOut(new PrintStream(outputStream));
+
+    String[] args = {
+      "metalake",
+      "list",
+      commandArg(GravitinoOptions.OUTPUT),
+      "table",
+      commandArg(GravitinoOptions.URL),
+      gravitinoUrl
+    };
+    Main.main(args);
+
+    // Restore the original System.out
+    System.setOut(originalOut);
+    // Get the output and verify it
+    String output = new String(outputStream.toByteArray(), 
StandardCharsets.UTF_8).trim();
+    assertEquals(
+        "+-------------+\n"
+            + "| metalake    |\n"
+            + "+-------------+\n"
+            + "| my_metalake |\n"
+            + "+-------------+",
+        output);
+  }
+
+  @Test
+  public void testMetalakeDetailsCommand() {
+    // Create a byte array output stream to capture the output of the command
+    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    PrintStream originalOut = System.out;
+    System.setOut(new PrintStream(outputStream));
+
+    String[] args = {
+      "metalake",
+      "details",
+      commandArg(GravitinoOptions.METALAKE),
+      "my_metalake",
+      commandArg(GravitinoOptions.OUTPUT),
+      "table",
+      commandArg(GravitinoOptions.URL),
+      gravitinoUrl
+    };
+    Main.main(args);
+
+    // Restore the original System.out
+    System.setOut(originalOut);
+    // Get the output and verify it
+    String output = new String(outputStream.toByteArray(), 
StandardCharsets.UTF_8).trim();
+    assertEquals(
+        "+-------------+-------------+\n"
+            + "| metalake    | comment     |\n"
+            + "+-------------+-------------+\n"
+            + "| my_metalake | my metalake |\n"
+            + "+-------------+-------------+",
+        output);
+  }
+
+  @Test
+  public void testCatalogDetailsCommand() {
+    // Create a byte array output stream to capture the output of the command
+    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    PrintStream originalOut = System.out;
+    System.setOut(new PrintStream(outputStream));
+
+    String[] args = {
+      "catalog",
+      "details",
+      commandArg(GravitinoOptions.METALAKE),
+      "my_metalake",
+      commandArg(GravitinoOptions.NAME),
+      "postgres",
+      commandArg(GravitinoOptions.OUTPUT),
+      "table",
+      commandArg(GravitinoOptions.URL),
+      gravitinoUrl
+    };
+    Main.main(args);
+
+    // Restore the original System.out
+    System.setOut(originalOut);
+    // Get the output and verify it
+    String output = new String(outputStream.toByteArray(), 
StandardCharsets.UTF_8).trim();
+    assertEquals(
+        "+----------+------------+-----------------+---------+\n"
+            + "| catalog  | type       | provider        | comment |\n"
+            + "+----------+------------+-----------------+---------+\n"
+            + "| postgres | RELATIONAL | jdbc-postgresql | null    |\n"
+            + "+----------+------------+-----------------+---------+",
+        output);
+  }
+
+  private String commandArg(String arg) {
+    return String.format("--%s", arg);
+  }
+}

Reply via email to