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

jmclean 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 e62c6d37a [#5528]Improvement[cli] Add and delete multiple tags at once 
in the Gravitino CLI (#5641)
e62c6d37a is described below

commit e62c6d37a4e52562dc5e512e780ccd4f3fb7bb14
Author: Xiaojian Sun <sunxiaojian...@163.com>
AuthorDate: Tue Nov 26 07:06:20 2024 +0800

    [#5528]Improvement[cli] Add and delete multiple tags at once in the 
Gravitino CLI (#5641)
    
    ### What changes were proposed in this pull request?
    
    Add and delete multiple tags at once in the Gravitino CLI
    
    ### Why are the changes needed?
    
    Close: [(#5528)](https://github.com/apache/gravitino/issues/5528)
---
 .../org/apache/gravitino/cli/ErrorMessages.java    |   3 +
 .../apache/gravitino/cli/GravitinoCommandLine.java |  31 ++--
 .../org/apache/gravitino/cli/GravitinoOptions.java |   7 +-
 .../apache/gravitino/cli/TestableCommandLine.java  |  18 +-
 .../apache/gravitino/cli/commands/CreateTag.java   |  56 +++++-
 .../apache/gravitino/cli/commands/DeleteTag.java   |  63 ++++++-
 .../apache/gravitino/cli/commands/TagEntity.java   |  23 +--
 .../apache/gravitino/cli/commands/UntagEntity.java |  25 +--
 .../org/apache/gravitino/cli/TestTagCommands.java  | 203 +++++++++++++++++++--
 docs/cli.md                                        |  16 +-
 10 files changed, 357 insertions(+), 88 deletions(-)

diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/ErrorMessages.java 
b/clients/cli/src/main/java/org/apache/gravitino/cli/ErrorMessages.java
index 0ad750f2c..6836bd203 100644
--- a/clients/cli/src/main/java/org/apache/gravitino/cli/ErrorMessages.java
+++ b/clients/cli/src/main/java/org/apache/gravitino/cli/ErrorMessages.java
@@ -38,7 +38,10 @@ public class ErrorMessages {
   public static final String UNKNOWN_GROUP = "Unknown group.";
   public static final String GROUP_EXISTS = "Group already exists.";
   public static final String UNKNOWN_TAG = "Unknown tag.";
+  public static final String MULTIPLE_TAG_COMMAND_ERROR =
+      "Error: The current command only supports one --tag option.";
   public static final String TAG_EXISTS = "Tag already exists.";
+  public static final String TAG_EMPTY = "Error: Must configure --tag option.";
   public static final String UNKNOWN_ROLE = "Unknown role.";
   public static final String ROLE_EXISTS = "Role already exists.";
   public static final String INVALID_SET_COMMAND =
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 c25b2f7e7..14e2cd20e 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
@@ -19,6 +19,8 @@
 
 package org.apache.gravitino.cli;
 
+import com.google.common.base.Preconditions;
+import java.util.Arrays;
 import java.util.Map;
 import org.apache.commons.cli.CommandLine;
 import org.apache.commons.cli.HelpFormatter;
@@ -366,10 +368,11 @@ public class GravitinoCommandLine extends 
TestableCommandLine {
     String url = getUrl();
     FullName name = new FullName(line);
     String metalake = name.getMetalakeName();
-    String tag = line.getOptionValue(GravitinoOptions.TAG);
 
+    String[] tags = line.getOptionValues(GravitinoOptions.TAG);
+    tags = tags != null ? 
Arrays.stream(tags).distinct().toArray(String[]::new) : null;
     if (CommandActions.DETAILS.equals(command)) {
-      newTagDetails(url, ignore, metalake, tag).handle();
+      newTagDetails(url, ignore, metalake, getOneTag(tags)).handle();
     } else if (CommandActions.LIST.equals(command)) {
       if (!name.hasCatalogName()) {
         newListTags(url, ignore, metalake).handle();
@@ -378,40 +381,44 @@ public class GravitinoCommandLine extends 
TestableCommandLine {
       }
     } else if (CommandActions.CREATE.equals(command)) {
       String comment = line.getOptionValue(GravitinoOptions.COMMENT);
-      newCreateTag(url, ignore, metalake, tag, comment).handle();
+      newCreateTags(url, ignore, metalake, tags, comment).handle();
     } else if (CommandActions.DELETE.equals(command)) {
       boolean force = line.hasOption(GravitinoOptions.FORCE);
-      newDeleteTag(url, ignore, force, metalake, tag).handle();
+      newDeleteTag(url, ignore, force, metalake, tags).handle();
     } else if (CommandActions.SET.equals(command)) {
       String property = line.getOptionValue(GravitinoOptions.PROPERTY);
       String value = line.getOptionValue(GravitinoOptions.VALUE);
-
       if (property != null && value != null) {
-        newSetTagProperty(url, ignore, metalake, tag, property, 
value).handle();
+        newSetTagProperty(url, ignore, metalake, getOneTag(tags), property, 
value).handle();
       } else if (name != null && property == null && value == null) {
-        newTagEntity(url, ignore, metalake, name, tag).handle();
+        newTagEntity(url, ignore, metalake, name, tags).handle();
       }
     } else if (CommandActions.REMOVE.equals(command)) {
       String property = line.getOptionValue(GravitinoOptions.PROPERTY);
       if (property != null) {
-        newRemoveTagProperty(url, ignore, metalake, tag, property).handle();
+        newRemoveTagProperty(url, ignore, metalake, getOneTag(tags), 
property).handle();
       } else {
-        newUntagEntity(url, ignore, metalake, name, tag).handle();
+        newUntagEntity(url, ignore, metalake, name, tags).handle();
       }
     } else if (CommandActions.PROPERTIES.equals(command)) {
-      newListTagProperties(url, ignore, metalake, tag).handle();
+      newListTagProperties(url, ignore, metalake, getOneTag(tags)).handle();
     } else if (CommandActions.UPDATE.equals(command)) {
       if (line.hasOption(GravitinoOptions.COMMENT)) {
         String comment = line.getOptionValue(GravitinoOptions.COMMENT);
-        newUpdateTagComment(url, ignore, metalake, tag, comment).handle();
+        newUpdateTagComment(url, ignore, metalake, getOneTag(tags), 
comment).handle();
       }
       if (line.hasOption(GravitinoOptions.RENAME)) {
         String newName = line.getOptionValue(GravitinoOptions.RENAME);
-        newUpdateTagName(url, ignore, metalake, tag, newName).handle();
+        newUpdateTagName(url, ignore, metalake, getOneTag(tags), 
newName).handle();
       }
     }
   }
 
+  private String getOneTag(String[] tags) {
+    Preconditions.checkArgument(tags.length <= 1, 
ErrorMessages.MULTIPLE_TAG_COMMAND_ERROR);
+    return tags[0];
+  }
+
   /** Handles the command execution for Roles based on command type and the 
command line options. */
   protected void handleRoleCommand() {
     String url = getUrl();
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 a6afdd589..8f017ba72 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
@@ -78,7 +78,7 @@ public class GravitinoOptions {
             "z", PROVIDER, "provider one of hadoop, hive, mysql, postgres, 
iceberg, kafka"));
     options.addOption(createArgOption("l", USER, "user name"));
     options.addOption(createArgOption("g", GROUP, "group name"));
-    options.addOption(createArgOption("t", TAG, "tag name"));
+    options.addOption(createArgsOption("t", TAG, "tag name"));
     options.addOption(createArgOption("r", ROLE, "role name"));
 
     // Properties option can have multiple values
@@ -115,4 +115,9 @@ public class GravitinoOptions {
   public Option createArgOption(String shortName, String longName, String 
description) {
     return new Option(shortName, longName, true, description);
   }
+
+  public Option createArgsOption(String shortName, String longName, String 
description) {
+    // Support multiple arguments
+    return 
Option.builder().option(shortName).longOpt(longName).hasArgs().desc(description).build();
+  }
 }
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 85bfa3203..9a4882b3a 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
@@ -370,14 +370,14 @@ public class TestableCommandLine {
     return new ListAllTags(url, ignore, metalake);
   }
 
-  protected CreateTag newCreateTag(
-      String url, boolean ignore, String metalake, String tag, String comment) 
{
-    return new CreateTag(url, ignore, metalake, tag, comment);
+  protected CreateTag newCreateTags(
+      String url, boolean ignore, String metalake, String[] tags, String 
comment) {
+    return new CreateTag(url, ignore, metalake, tags, comment);
   }
 
   protected DeleteTag newDeleteTag(
-      String url, boolean ignore, boolean force, String metalake, String tag) {
-    return new DeleteTag(url, ignore, force, metalake, tag);
+      String url, boolean ignore, boolean force, String metalake, String[] 
tags) {
+    return new DeleteTag(url, ignore, force, metalake, tags);
   }
 
   protected SetTagProperty newSetTagProperty(
@@ -411,13 +411,13 @@ public class TestableCommandLine {
   }
 
   protected TagEntity newTagEntity(
-      String url, boolean ignore, String metalake, FullName name, String tag) {
-    return new TagEntity(url, ignore, metalake, name, tag);
+      String url, boolean ignore, String metalake, FullName name, String[] 
tags) {
+    return new TagEntity(url, ignore, metalake, name, tags);
   }
 
   protected UntagEntity newUntagEntity(
-      String url, boolean ignore, String metalake, FullName name, String tag) {
-    return new UntagEntity(url, ignore, metalake, name, tag);
+      String url, boolean ignore, String metalake, FullName name, String[] 
tags) {
+    return new UntagEntity(url, ignore, metalake, name, tags);
   }
 
   protected ListColumns newListColumns(
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/CreateTag.java 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/CreateTag.java
index 76f6f85b4..004254c16 100644
--- a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/CreateTag.java
+++ b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/CreateTag.java
@@ -19,6 +19,9 @@
 
 package org.apache.gravitino.cli.commands;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
 import org.apache.gravitino.cli.ErrorMessages;
 import org.apache.gravitino.client.GravitinoClient;
 import org.apache.gravitino.exceptions.NoSuchMetalakeException;
@@ -26,32 +29,41 @@ import 
org.apache.gravitino.exceptions.TagAlreadyExistsException;
 
 public class CreateTag extends Command {
   protected final String metalake;
-  protected final String tag;
+  protected final String[] tags;
   protected final String comment;
 
   /**
-   * Create a new tag.
+   * Create tags.
    *
    * @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 tag The name of the tag.
+   * @param tags The names of the tags.
    * @param comment The comment of the tag.
    */
   public CreateTag(
-      String url, boolean ignoreVersions, String metalake, String tag, String 
comment) {
+      String url, boolean ignoreVersions, String metalake, String[] tags, 
String comment) {
     super(url, ignoreVersions);
     this.metalake = metalake;
-    this.tag = tag;
+    this.tags = tags;
     this.comment = comment;
   }
 
-  /** Create a new tag. */
+  /** Create tags. */
   @Override
   public void handle() {
+    boolean hasOnlyOneTag = tags.length == 1;
+    if (hasOnlyOneTag) {
+      handleOnlyOneTag();
+    } else {
+      handleMultipleTags();
+    }
+  }
+
+  private void handleOnlyOneTag() {
     try {
       GravitinoClient client = buildClient(metalake);
-      client.createTag(tag, comment, null);
+      client.createTag(tags[0], comment, null);
     } catch (NoSuchMetalakeException err) {
       System.err.println(ErrorMessages.UNKNOWN_METALAKE);
       return;
@@ -63,6 +75,34 @@ public class CreateTag extends Command {
       return;
     }
 
-    System.out.println(tag + " created");
+    System.out.println(tags[0] + " created");
+  }
+
+  private void handleMultipleTags() {
+    List<String> created = new ArrayList<>();
+    try {
+      GravitinoClient client = buildClient(metalake);
+      for (String tag : tags) {
+        client.createTag(tag, comment, null);
+        created.add(tag);
+      }
+    } catch (NoSuchMetalakeException err) {
+      System.err.println(ErrorMessages.UNKNOWN_METALAKE);
+      return;
+    } catch (TagAlreadyExistsException err) {
+      System.err.println(ErrorMessages.TAG_EXISTS);
+      return;
+    } catch (Exception exp) {
+      System.err.println(exp.getMessage());
+      return;
+    }
+    if (!created.isEmpty()) {
+      System.out.println("Tags " + String.join(",", created) + " created");
+    }
+    if (created.size() < tags.length) {
+      List<String> remaining = Arrays.asList(tags);
+      remaining.removeAll(created);
+      System.out.println("Tags " + String.join(",", remaining) + " not 
created");
+    }
   }
 }
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/DeleteTag.java 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/DeleteTag.java
index 4536897b0..0db4a8976 100644
--- a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/DeleteTag.java
+++ b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/DeleteTag.java
@@ -19,6 +19,9 @@
 
 package org.apache.gravitino.cli.commands;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
 import org.apache.gravitino.cli.AreYouSure;
 import org.apache.gravitino.cli.ErrorMessages;
 import org.apache.gravitino.client.GravitinoClient;
@@ -28,28 +31,70 @@ import org.apache.gravitino.exceptions.NoSuchTagException;
 public class DeleteTag extends Command {
 
   protected final String metalake;
-  protected final String tag;
+  protected final String[] tags;
   protected final boolean force;
 
   /**
-   * Delete a tag.
+   * Delete tags.
    *
    * @param url The URL of the Gravitino server.
    * @param ignoreVersions If true don't check the client/server versions 
match.
    * @param force Force operation.
    * @param metalake The name of the metalake.
-   * @param tag The name of the tag.
+   * @param tags The names of the tags.
    */
-  public DeleteTag(String url, boolean ignoreVersions, boolean force, String 
metalake, String tag) {
+  public DeleteTag(
+      String url, boolean ignoreVersions, boolean force, String metalake, 
String[] tags) {
     super(url, ignoreVersions);
     this.force = force;
     this.metalake = metalake;
-    this.tag = tag;
+    this.tags = tags;
   }
 
-  /** Delete a tag. */
+  /** Delete tags. */
   @Override
   public void handle() {
+    if (!AreYouSure.really(force)) {
+      return;
+    }
+    boolean hasOnlyOneTag = tags.length == 1;
+    if (hasOnlyOneTag) {
+      handleOnlyOneTag();
+    } else {
+      handleMultipleTags();
+    }
+  }
+
+  private void handleMultipleTags() {
+    List<String> deleted = new ArrayList<>();
+    try {
+      GravitinoClient client = buildClient(metalake);
+      for (String tag : tags) {
+        if (client.deleteTag(tag)) {
+          deleted.add(tag);
+        }
+      }
+    } catch (NoSuchMetalakeException err) {
+      System.err.println(ErrorMessages.UNKNOWN_METALAKE);
+      return;
+    } catch (NoSuchTagException err) {
+      System.err.println(ErrorMessages.UNKNOWN_TAG);
+      return;
+    } catch (Exception exp) {
+      System.err.println(exp.getMessage());
+      return;
+    }
+    if (!deleted.isEmpty()) {
+      System.out.println("Tags " + String.join(",", deleted) + " deleted.");
+    }
+    if (deleted.size() < tags.length) {
+      List<String> remaining = Arrays.asList(tags);
+      remaining.removeAll(deleted);
+      System.out.println("Tags " + String.join(",", deleted) + " not 
deleted.");
+    }
+  }
+
+  private void handleOnlyOneTag() {
     boolean deleted = false;
 
     if (!AreYouSure.really(force)) {
@@ -58,7 +103,7 @@ public class DeleteTag extends Command {
 
     try {
       GravitinoClient client = buildClient(metalake);
-      deleted = client.deleteTag(tag);
+      deleted = client.deleteTag(tags[0]);
     } catch (NoSuchMetalakeException err) {
       System.err.println(ErrorMessages.UNKNOWN_METALAKE);
       return;
@@ -71,9 +116,9 @@ public class DeleteTag extends Command {
     }
 
     if (deleted) {
-      System.out.println(tag + " deleted.");
+      System.out.println(tags[0] + " deleted.");
     } else {
-      System.out.println(tag + " not deleted.");
+      System.out.println(tags[0] + " not deleted.");
     }
   }
 }
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/TagEntity.java 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/TagEntity.java
index 08e1d2ca1..ed474c784 100644
--- a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/TagEntity.java
+++ b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/TagEntity.java
@@ -34,29 +34,30 @@ import org.apache.gravitino.rel.Table;
 public class TagEntity extends Command {
   protected final String metalake;
   protected final FullName name;
-  protected final String tag;
+  protected final String[] tags;
 
   /**
-   * Tag an entity with an existing tag.
+   * Tag an entity with existing tags.
    *
    * @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 name The name of the entity.
-   * @param tag The name of the tag.
+   * @param tags The names of the tags.
    */
-  public TagEntity(String url, boolean ignoreVersions, String metalake, 
FullName name, String tag) {
+  public TagEntity(
+      String url, boolean ignoreVersions, String metalake, FullName name, 
String[] tags) {
     super(url, ignoreVersions);
     this.metalake = metalake;
     this.name = name;
-    this.tag = tag;
+    this.tags = tags;
   }
 
-  /** Create a new tag. */
+  /** Add tags for an entity. */
   @Override
   public void handle() {
     String entity = "unknown";
-    String[] tags = new String[0];
+    String[] tagsToAdd = new String[0];
 
     try {
       GravitinoClient client = buildClient(metalake);
@@ -71,18 +72,18 @@ public class TagEntity extends Command {
                 .loadCatalog(catalog)
                 .asTableCatalog()
                 .loadTable(NameIdentifier.of(schema, table));
-        tags = gTable.supportsTags().associateTags(new String[] {tag}, null);
+        tagsToAdd = gTable.supportsTags().associateTags(tags, null);
         entity = table;
       } else if (name.hasSchemaName()) {
         String catalog = name.getCatalogName();
         String schema = name.getSchemaName();
         Schema gSchema = 
client.loadCatalog(catalog).asSchemas().loadSchema(schema);
-        tags = gSchema.supportsTags().associateTags(new String[] {tag}, null);
+        tagsToAdd = gSchema.supportsTags().associateTags(tags, null);
         entity = schema;
       } else if (name.hasCatalogName()) {
         String catalog = name.getCatalogName();
         Catalog gCatalog = client.loadCatalog(catalog);
-        tags = gCatalog.supportsTags().associateTags(new String[] {tag}, null);
+        tagsToAdd = gCatalog.supportsTags().associateTags(tags, null);
         entity = catalog;
       }
     } catch (NoSuchMetalakeException err) {
@@ -102,7 +103,7 @@ public class TagEntity extends Command {
       return;
     }
 
-    String all = String.join(",", tags);
+    String all = String.join(",", tagsToAdd);
 
     System.out.println(entity + " tagged with " + all);
   }
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/UntagEntity.java 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/UntagEntity.java
index 91b9fcb15..77437dafc 100644
--- 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/UntagEntity.java
+++ 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/UntagEntity.java
@@ -34,30 +34,30 @@ import org.apache.gravitino.rel.Table;
 public class UntagEntity extends Command {
   protected final String metalake;
   protected final FullName name;
-  protected final String tag;
+  protected final String[] tags;
 
   /**
-   * Untag an entity with an existing tag.
+   * Remove existing tags from an entity.
    *
    * @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 name The name of the entity.
-   * @param tag The name of the tag.
+   * @param tags The names of the tags.
    */
   public UntagEntity(
-      String url, boolean ignoreVersions, String metalake, FullName name, 
String tag) {
+      String url, boolean ignoreVersions, String metalake, FullName name, 
String[] tags) {
     super(url, ignoreVersions);
     this.metalake = metalake;
     this.name = name;
-    this.tag = tag;
+    this.tags = tags;
   }
 
-  /** Create a new tag. */
+  /** Remove tags from an entity. */
   @Override
   public void handle() {
     String entity = "unknown";
-    String[] tags = new String[0];
+    String[] removeTags = new String[0];
 
     try {
       GravitinoClient client = buildClient(metalake);
@@ -72,18 +72,18 @@ public class UntagEntity extends Command {
                 .loadCatalog(catalog)
                 .asTableCatalog()
                 .loadTable(NameIdentifier.of(schema, table));
-        tags = gTable.supportsTags().associateTags(null, new String[] {tag});
+        removeTags = gTable.supportsTags().associateTags(null, tags);
         entity = table;
       } else if (name.hasSchemaName()) {
         String catalog = name.getCatalogName();
         String schema = name.getSchemaName();
         Schema gSchema = 
client.loadCatalog(catalog).asSchemas().loadSchema(schema);
-        tags = gSchema.supportsTags().associateTags(null, new String[] {tag});
+        removeTags = gSchema.supportsTags().associateTags(null, tags);
         entity = schema;
       } else if (name.hasCatalogName()) {
         String catalog = name.getCatalogName();
         Catalog gCatalog = client.loadCatalog(catalog);
-        tags = gCatalog.supportsTags().associateTags(null, new String[] {tag});
+        removeTags = gCatalog.supportsTags().associateTags(null, tags);
         entity = catalog;
       }
     } catch (NoSuchMetalakeException err) {
@@ -103,12 +103,13 @@ public class UntagEntity extends Command {
       return;
     }
 
-    String all = String.join(",", tags);
+    String all = String.join(",", removeTags);
 
     if (all.equals("")) {
       all = "nothing";
     }
 
-    System.out.println(entity + " removed tag " + tag + ", now tagged with " + 
all);
+    System.out.println(
+        entity + " removed tag " + String.join(",", tags) + " now tagged with 
" + all);
   }
 }
diff --git 
a/clients/cli/src/test/java/org/apache/gravitino/cli/TestTagCommands.java 
b/clients/cli/src/test/java/org/apache/gravitino/cli/TestTagCommands.java
index 3dfd0392c..91a809fbc 100644
--- a/clients/cli/src/test/java/org/apache/gravitino/cli/TestTagCommands.java
+++ b/clients/cli/src/test/java/org/apache/gravitino/cli/TestTagCommands.java
@@ -19,6 +19,7 @@
 
 package org.apache.gravitino.cli;
 
+import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.eq;
@@ -41,8 +42,10 @@ import org.apache.gravitino.cli.commands.TagEntity;
 import org.apache.gravitino.cli.commands.UntagEntity;
 import org.apache.gravitino.cli.commands.UpdateTagComment;
 import org.apache.gravitino.cli.commands.UpdateTagName;
+import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentMatcher;
 
 class TestTagCommands {
   private CommandLine mockCommandLine;
@@ -75,7 +78,7 @@ class TestTagCommands {
     TagDetails mockDetails = mock(TagDetails.class);
     
when(mockCommandLine.hasOption(GravitinoOptions.METALAKE)).thenReturn(true);
     
when(mockCommandLine.getOptionValue(GravitinoOptions.METALAKE)).thenReturn("metalake_demo");
-    when(mockCommandLine.hasOption(GravitinoOptions.TAG)).thenReturn(true);
+    when(mockCommandLine.getOptionValues(GravitinoOptions.TAG)).thenReturn(new 
String[] {"tagA"});
     
when(mockCommandLine.getOptionValue(GravitinoOptions.TAG)).thenReturn("tagA");
     GravitinoCommandLine commandLine =
         spy(
@@ -94,7 +97,33 @@ class TestTagCommands {
     
when(mockCommandLine.hasOption(GravitinoOptions.METALAKE)).thenReturn(true);
     
when(mockCommandLine.getOptionValue(GravitinoOptions.METALAKE)).thenReturn("metalake_demo");
     when(mockCommandLine.hasOption(GravitinoOptions.TAG)).thenReturn(true);
-    
when(mockCommandLine.getOptionValue(GravitinoOptions.TAG)).thenReturn("tagA");
+    when(mockCommandLine.getOptionValues(GravitinoOptions.TAG)).thenReturn(new 
String[] {"tagA"});
+    when(mockCommandLine.hasOption(GravitinoOptions.COMMENT)).thenReturn(true);
+    
when(mockCommandLine.getOptionValue(GravitinoOptions.COMMENT)).thenReturn("comment");
+    GravitinoCommandLine commandLine =
+        spy(
+            new GravitinoCommandLine(
+                mockCommandLine, mockOptions, CommandEntities.TAG, 
CommandActions.CREATE));
+    doReturn(mockCreate)
+        .when(commandLine)
+        .newCreateTags(
+            GravitinoCommandLine.DEFAULT_URL,
+            false,
+            "metalake_demo",
+            new String[] {"tagA"},
+            "comment");
+    commandLine.handleCommandLine();
+    verify(mockCreate).handle();
+  }
+
+  @Test
+  void testCreateTagsCommand() {
+    CreateTag mockCreate = mock(CreateTag.class);
+    
when(mockCommandLine.hasOption(GravitinoOptions.METALAKE)).thenReturn(true);
+    
when(mockCommandLine.getOptionValue(GravitinoOptions.METALAKE)).thenReturn("metalake_demo");
+    when(mockCommandLine.hasOption(GravitinoOptions.TAG)).thenReturn(true);
+    when(mockCommandLine.getOptionValues(GravitinoOptions.TAG))
+        .thenReturn(new String[] {"tagA", "tagB"});
     when(mockCommandLine.hasOption(GravitinoOptions.COMMENT)).thenReturn(true);
     
when(mockCommandLine.getOptionValue(GravitinoOptions.COMMENT)).thenReturn("comment");
     GravitinoCommandLine commandLine =
@@ -103,7 +132,12 @@ class TestTagCommands {
                 mockCommandLine, mockOptions, CommandEntities.TAG, 
CommandActions.CREATE));
     doReturn(mockCreate)
         .when(commandLine)
-        .newCreateTag(GravitinoCommandLine.DEFAULT_URL, false, 
"metalake_demo", "tagA", "comment");
+        .newCreateTags(
+            GravitinoCommandLine.DEFAULT_URL,
+            false,
+            "metalake_demo",
+            new String[] {"tagA", "tagB"},
+            "comment");
     commandLine.handleCommandLine();
     verify(mockCreate).handle();
   }
@@ -114,14 +148,15 @@ class TestTagCommands {
     
when(mockCommandLine.hasOption(GravitinoOptions.METALAKE)).thenReturn(true);
     
when(mockCommandLine.getOptionValue(GravitinoOptions.METALAKE)).thenReturn("metalake_demo");
     when(mockCommandLine.hasOption(GravitinoOptions.TAG)).thenReturn(true);
-    
when(mockCommandLine.getOptionValue(GravitinoOptions.TAG)).thenReturn("tagA");
+    when(mockCommandLine.getOptionValues(GravitinoOptions.TAG)).thenReturn(new 
String[] {"tagA"});
     GravitinoCommandLine commandLine =
         spy(
             new GravitinoCommandLine(
                 mockCommandLine, mockOptions, CommandEntities.TAG, 
CommandActions.CREATE));
     doReturn(mockCreate)
         .when(commandLine)
-        .newCreateTag(GravitinoCommandLine.DEFAULT_URL, false, 
"metalake_demo", "tagA", null);
+        .newCreateTags(
+            GravitinoCommandLine.DEFAULT_URL, false, "metalake_demo", new 
String[] {"tagA"}, null);
     commandLine.handleCommandLine();
     verify(mockCreate).handle();
   }
@@ -132,14 +167,39 @@ class TestTagCommands {
     
when(mockCommandLine.hasOption(GravitinoOptions.METALAKE)).thenReturn(true);
     
when(mockCommandLine.getOptionValue(GravitinoOptions.METALAKE)).thenReturn("metalake_demo");
     when(mockCommandLine.hasOption(GravitinoOptions.TAG)).thenReturn(true);
-    
when(mockCommandLine.getOptionValue(GravitinoOptions.TAG)).thenReturn("tagA");
+    when(mockCommandLine.getOptionValues(GravitinoOptions.TAG)).thenReturn(new 
String[] {"tagA"});
+    GravitinoCommandLine commandLine =
+        spy(
+            new GravitinoCommandLine(
+                mockCommandLine, mockOptions, CommandEntities.TAG, 
CommandActions.DELETE));
+    doReturn(mockDelete)
+        .when(commandLine)
+        .newDeleteTag(
+            GravitinoCommandLine.DEFAULT_URL, false, false, "metalake_demo", 
new String[] {"tagA"});
+    commandLine.handleCommandLine();
+    verify(mockDelete).handle();
+  }
+
+  @Test
+  void testDeleteTagsCommand() {
+    DeleteTag mockDelete = mock(DeleteTag.class);
+    
when(mockCommandLine.hasOption(GravitinoOptions.METALAKE)).thenReturn(true);
+    
when(mockCommandLine.getOptionValue(GravitinoOptions.METALAKE)).thenReturn("metalake_demo");
+    when(mockCommandLine.hasOption(GravitinoOptions.TAG)).thenReturn(true);
+    when(mockCommandLine.getOptionValues(GravitinoOptions.TAG))
+        .thenReturn(new String[] {"tagA", "tagB"});
     GravitinoCommandLine commandLine =
         spy(
             new GravitinoCommandLine(
                 mockCommandLine, mockOptions, CommandEntities.TAG, 
CommandActions.DELETE));
     doReturn(mockDelete)
         .when(commandLine)
-        .newDeleteTag(GravitinoCommandLine.DEFAULT_URL, false, false, 
"metalake_demo", "tagA");
+        .newDeleteTag(
+            GravitinoCommandLine.DEFAULT_URL,
+            false,
+            false,
+            "metalake_demo",
+            new String[] {"tagA", "tagB"});
     commandLine.handleCommandLine();
     verify(mockDelete).handle();
   }
@@ -150,7 +210,7 @@ class TestTagCommands {
     
when(mockCommandLine.hasOption(GravitinoOptions.METALAKE)).thenReturn(true);
     
when(mockCommandLine.getOptionValue(GravitinoOptions.METALAKE)).thenReturn("metalake_demo");
     when(mockCommandLine.hasOption(GravitinoOptions.TAG)).thenReturn(true);
-    
when(mockCommandLine.getOptionValue(GravitinoOptions.TAG)).thenReturn("tagA");
+    when(mockCommandLine.getOptionValues(GravitinoOptions.TAG)).thenReturn(new 
String[] {"tagA"});
     when(mockCommandLine.hasOption(GravitinoOptions.FORCE)).thenReturn(true);
     GravitinoCommandLine commandLine =
         spy(
@@ -158,7 +218,8 @@ class TestTagCommands {
                 mockCommandLine, mockOptions, CommandEntities.TAG, 
CommandActions.DELETE));
     doReturn(mockDelete)
         .when(commandLine)
-        .newDeleteTag(GravitinoCommandLine.DEFAULT_URL, false, true, 
"metalake_demo", "tagA");
+        .newDeleteTag(
+            GravitinoCommandLine.DEFAULT_URL, false, true, "metalake_demo", 
new String[] {"tagA"});
     commandLine.handleCommandLine();
     verify(mockDelete).handle();
   }
@@ -169,7 +230,7 @@ class TestTagCommands {
     
when(mockCommandLine.hasOption(GravitinoOptions.METALAKE)).thenReturn(true);
     
when(mockCommandLine.getOptionValue(GravitinoOptions.METALAKE)).thenReturn("metalake_demo");
     when(mockCommandLine.hasOption(GravitinoOptions.TAG)).thenReturn(true);
-    
when(mockCommandLine.getOptionValue(GravitinoOptions.TAG)).thenReturn("tagA");
+    when(mockCommandLine.getOptionValues(GravitinoOptions.TAG)).thenReturn(new 
String[] {"tagA"});
     
when(mockCommandLine.hasOption(GravitinoOptions.PROPERTY)).thenReturn(true);
     
when(mockCommandLine.getOptionValue(GravitinoOptions.PROPERTY)).thenReturn("property");
     when(mockCommandLine.hasOption(GravitinoOptions.VALUE)).thenReturn(true);
@@ -186,13 +247,34 @@ class TestTagCommands {
     verify(mockSetProperty).handle();
   }
 
+  @Test
+  void testSetMultipleTagPropertyCommandError() {
+    
when(mockCommandLine.hasOption(GravitinoOptions.METALAKE)).thenReturn(true);
+    
when(mockCommandLine.getOptionValue(GravitinoOptions.METALAKE)).thenReturn("metalake_demo");
+    when(mockCommandLine.hasOption(GravitinoOptions.TAG)).thenReturn(true);
+    when(mockCommandLine.getOptionValues(GravitinoOptions.TAG))
+        .thenReturn(new String[] {"tagA", "tagB"});
+    
when(mockCommandLine.hasOption(GravitinoOptions.PROPERTY)).thenReturn(true);
+    
when(mockCommandLine.getOptionValue(GravitinoOptions.PROPERTY)).thenReturn("property");
+    when(mockCommandLine.hasOption(GravitinoOptions.VALUE)).thenReturn(true);
+    
when(mockCommandLine.getOptionValue(GravitinoOptions.VALUE)).thenReturn("value");
+    GravitinoCommandLine commandLine =
+        spy(
+            new GravitinoCommandLine(
+                mockCommandLine, mockOptions, CommandEntities.TAG, 
CommandActions.SET));
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () -> commandLine.handleCommandLine(),
+        "Error: The current command only supports one --tag option.");
+  }
+
   @Test
   void testRemoveTagPropertyCommand() {
     RemoveTagProperty mockRemoveProperty = mock(RemoveTagProperty.class);
     
when(mockCommandLine.hasOption(GravitinoOptions.METALAKE)).thenReturn(true);
     
when(mockCommandLine.getOptionValue(GravitinoOptions.METALAKE)).thenReturn("metalake_demo");
     when(mockCommandLine.hasOption(GravitinoOptions.TAG)).thenReturn(true);
-    
when(mockCommandLine.getOptionValue(GravitinoOptions.TAG)).thenReturn("tagA");
+    when(mockCommandLine.getOptionValues(GravitinoOptions.TAG)).thenReturn(new 
String[] {"tagA"});
     
when(mockCommandLine.hasOption(GravitinoOptions.PROPERTY)).thenReturn(true);
     
when(mockCommandLine.getOptionValue(GravitinoOptions.PROPERTY)).thenReturn("property");
     GravitinoCommandLine commandLine =
@@ -213,7 +295,7 @@ class TestTagCommands {
     
when(mockCommandLine.hasOption(GravitinoOptions.METALAKE)).thenReturn(true);
     
when(mockCommandLine.getOptionValue(GravitinoOptions.METALAKE)).thenReturn("metalake_demo");
     when(mockCommandLine.hasOption(GravitinoOptions.TAG)).thenReturn(true);
-    
when(mockCommandLine.getOptionValue(GravitinoOptions.TAG)).thenReturn("tagA");
+    when(mockCommandLine.getOptionValues(GravitinoOptions.TAG)).thenReturn(new 
String[] {"tagA"});
     GravitinoCommandLine commandLine =
         spy(
             new GravitinoCommandLine(
@@ -232,7 +314,7 @@ class TestTagCommands {
     
when(mockCommandLine.getOptionValue(GravitinoOptions.METALAKE)).thenReturn("metalake_demo");
     when(mockCommandLine.hasOption(GravitinoOptions.COMMENT)).thenReturn(true);
     when(mockCommandLine.hasOption(GravitinoOptions.TAG)).thenReturn(true);
-    
when(mockCommandLine.getOptionValue(GravitinoOptions.TAG)).thenReturn("tagA");
+    when(mockCommandLine.getOptionValues(GravitinoOptions.TAG)).thenReturn(new 
String[] {"tagA"});
     
when(mockCommandLine.getOptionValue(GravitinoOptions.COMMENT)).thenReturn("new 
comment");
     GravitinoCommandLine commandLine =
         spy(
@@ -252,7 +334,7 @@ class TestTagCommands {
     
when(mockCommandLine.hasOption(GravitinoOptions.METALAKE)).thenReturn(true);
     
when(mockCommandLine.getOptionValue(GravitinoOptions.METALAKE)).thenReturn("metalake_demo");
     when(mockCommandLine.hasOption(GravitinoOptions.TAG)).thenReturn(true);
-    
when(mockCommandLine.getOptionValue(GravitinoOptions.TAG)).thenReturn("tagA");
+    when(mockCommandLine.getOptionValues(GravitinoOptions.TAG)).thenReturn(new 
String[] {"tagA"});
     when(mockCommandLine.hasOption(GravitinoOptions.RENAME)).thenReturn(true);
     
when(mockCommandLine.getOptionValue(GravitinoOptions.RENAME)).thenReturn("tagB");
     GravitinoCommandLine commandLine =
@@ -293,7 +375,39 @@ class TestTagCommands {
     when(mockCommandLine.hasOption(GravitinoOptions.NAME)).thenReturn(true);
     
when(mockCommandLine.getOptionValue(GravitinoOptions.NAME)).thenReturn("catalog.schema.table");
     when(mockCommandLine.hasOption(GravitinoOptions.TAG)).thenReturn(true);
-    
when(mockCommandLine.getOptionValue(GravitinoOptions.TAG)).thenReturn("tagA");
+    when(mockCommandLine.getOptionValues(GravitinoOptions.TAG)).thenReturn(new 
String[] {"tagA"});
+    GravitinoCommandLine commandLine =
+        spy(
+            new GravitinoCommandLine(
+                mockCommandLine, mockOptions, CommandEntities.TAG, 
CommandActions.SET));
+    doReturn(mockTagEntity)
+        .when(commandLine)
+        .newTagEntity(
+            eq(GravitinoCommandLine.DEFAULT_URL),
+            eq(false),
+            eq("metalake_demo"),
+            any(),
+            argThat(
+                new ArgumentMatcher<String[]>() {
+                  @Override
+                  public boolean matches(String[] argument) {
+                    return argument != null && argument.length > 0 && 
"tagA".equals(argument[0]);
+                  }
+                }));
+    commandLine.handleCommandLine();
+    verify(mockTagEntity).handle();
+  }
+
+  @Test
+  void testTagsEntityCommand() {
+    TagEntity mockTagEntity = mock(TagEntity.class);
+    
when(mockCommandLine.hasOption(GravitinoOptions.METALAKE)).thenReturn(true);
+    
when(mockCommandLine.getOptionValue(GravitinoOptions.METALAKE)).thenReturn("metalake_demo");
+    when(mockCommandLine.hasOption(GravitinoOptions.NAME)).thenReturn(true);
+    
when(mockCommandLine.getOptionValue(GravitinoOptions.NAME)).thenReturn("catalog.schema.table");
+    when(mockCommandLine.hasOption(GravitinoOptions.TAG)).thenReturn(true);
+    when(mockCommandLine.getOptionValues(GravitinoOptions.TAG))
+        .thenReturn(new String[] {"tagA", "tagB"});
     GravitinoCommandLine commandLine =
         spy(
             new GravitinoCommandLine(
@@ -305,7 +419,16 @@ class TestTagCommands {
             eq(false),
             eq("metalake_demo"),
             any(),
-            eq("tagA"));
+            argThat(
+                new ArgumentMatcher<String[]>() {
+                  @Override
+                  public boolean matches(String[] argument) {
+                    return argument != null
+                        && argument.length == 2
+                        && "tagA".equals(argument[0])
+                        && "tagB".equals(argument[1]);
+                  }
+                }));
     commandLine.handleCommandLine();
     verify(mockTagEntity).handle();
   }
@@ -318,7 +441,42 @@ class TestTagCommands {
     when(mockCommandLine.hasOption(GravitinoOptions.NAME)).thenReturn(true);
     
when(mockCommandLine.getOptionValue(GravitinoOptions.NAME)).thenReturn("catalog.schema.table");
     when(mockCommandLine.hasOption(GravitinoOptions.TAG)).thenReturn(true);
-    
when(mockCommandLine.getOptionValue(GravitinoOptions.TAG)).thenReturn("tagA");
+    when(mockCommandLine.getOptionValues(GravitinoOptions.TAG))
+        .thenReturn(new String[] {"tagA", "tagB"});
+    
when(mockCommandLine.hasOption(GravitinoOptions.PROPERTY)).thenReturn(false);
+    
when(mockCommandLine.getOptionValue(GravitinoOptions.PROPERTY)).thenReturn(null);
+    GravitinoCommandLine commandLine =
+        spy(
+            new GravitinoCommandLine(
+                mockCommandLine, mockOptions, CommandEntities.TAG, 
CommandActions.REMOVE));
+    doReturn(mockUntagEntity)
+        .when(commandLine)
+        .newUntagEntity(
+            eq(GravitinoCommandLine.DEFAULT_URL),
+            eq(false),
+            eq("metalake_demo"),
+            any(),
+            argThat(
+                new ArgumentMatcher<String[]>() {
+                  @Override
+                  public boolean matches(String[] argument) {
+                    return argument != null && argument.length > 0 && 
"tagA".equals(argument[0]);
+                  }
+                }));
+    commandLine.handleCommandLine();
+    verify(mockUntagEntity).handle();
+  }
+
+  @Test
+  void testUntagsEntityCommand() {
+    UntagEntity mockUntagEntity = mock(UntagEntity.class);
+    
when(mockCommandLine.hasOption(GravitinoOptions.METALAKE)).thenReturn(true);
+    
when(mockCommandLine.getOptionValue(GravitinoOptions.METALAKE)).thenReturn("metalake_demo");
+    when(mockCommandLine.hasOption(GravitinoOptions.NAME)).thenReturn(true);
+    
when(mockCommandLine.getOptionValue(GravitinoOptions.NAME)).thenReturn("catalog.schema.table");
+    when(mockCommandLine.hasOption(GravitinoOptions.TAG)).thenReturn(true);
+    when(mockCommandLine.getOptionValues(GravitinoOptions.TAG))
+        .thenReturn(new String[] {"tagA", "tagB"});
     
when(mockCommandLine.hasOption(GravitinoOptions.PROPERTY)).thenReturn(false);
     
when(mockCommandLine.getOptionValue(GravitinoOptions.PROPERTY)).thenReturn(null);
     GravitinoCommandLine commandLine =
@@ -332,7 +490,16 @@ class TestTagCommands {
             eq(false),
             eq("metalake_demo"),
             any(),
-            eq("tagA"));
+            argThat(
+                new ArgumentMatcher<String[]>() {
+                  @Override
+                  public boolean matches(String[] argument) {
+                    return argument != null
+                        && argument.length == 2
+                        && "tagA".equals(argument[0])
+                        && "tagB".equals(argument[1]);
+                  }
+                }));
     commandLine.handleCommandLine();
     verify(mockUntagEntity).handle();
   }
diff --git a/docs/cli.md b/docs/cli.md
index d9862df9b..e37e92e10 100644
--- a/docs/cli.md
+++ b/docs/cli.md
@@ -456,10 +456,10 @@ gcli group delete --group new_group
 gcli tag details --tag tagA
 ```
 
-#### Create a tag
+#### Create tags
 
 ```bash
- gcli tag create --tag tagA
+ gcli tag create --tag tagA tagB
  ```
 
 #### List all tag
@@ -468,22 +468,22 @@ gcli tag details --tag tagA
 gcli tag list
 ```
 
-#### Delete a tag
+#### Delete tags
 
 ```bash
-gcli tag delete --tag tagA
+gcli tag delete --tag tagA tagB
 ```
 
-#### Add a tag to an entity
+#### Add tags to an entity
 
 ```bash
-gcli tag set --name catalog_postgres.hr --tag tagA
+gcli tag set --name catalog_postgres.hr --tag tagA tagB
 ```
 
-#### Remove a tag from an entity
+#### Remove tags from an entity
 
 ```bash
-gcli tag remove --name catalog_postgres.hr --tag tagA
+gcli tag remove --name catalog_postgres.hr --tag tagA tagB
 ```
 
 #### List all tags on an entity


Reply via email to