[commissioner] add cli command to print joiner list (#7521)

diff --git a/src/cli/README_COMMISSIONER.md b/src/cli/README_COMMISSIONER.md
index 7cd7079..d94c125 100644
--- a/src/cli/README_COMMISSIONER.md
+++ b/src/cli/README_COMMISSIONER.md
@@ -11,6 +11,7 @@
 - [energy](#energy)
 - [joiner add](#joiner-add)
 - [joiner remove](#joiner-remove)
+- [joiner table](#joiner-table)
 - [mgmtget](#mgmtget)
 - [mgmtset](#mgmtset)
 - [panid](#panid)
@@ -120,6 +121,22 @@
 Done
 ```
 
+### joiner table
+
+Usage: `commissioner joiner table`
+
+List all Joiner entries.
+
+```bash
+> commissioner joiner table
+| ID                    | PSKd                             | Expiration |
++-----------------------+----------------------------------+------------+
+|                     * |                           J01NME |      81015 |
+|      d45e64fa83f81cf7 |                           J01NME |     101204 |
+| 0x0000000000000abc/12 |                           J01NME |     114360 |
+Done
+```
+
 ### mgmtget
 
 Usage: `commissioner mgmtget [locator] [sessionid] [steeringdata] [joinerudpport] [-x <TLV Types>]`
diff --git a/src/cli/cli_commissioner.cpp b/src/cli/cli_commissioner.cpp
index f9a6e3b..5db0425 100644
--- a/src/cli/cli_commissioner.cpp
+++ b/src/cli/cli_commissioner.cpp
@@ -88,6 +88,47 @@
     const otExtAddress *addrPtr = nullptr;
     otJoinerDiscerner   discerner;
 
+    if (aArgs[0] == "table")
+    {
+        uint16_t     iter = 0;
+        otJoinerInfo joinerInfo;
+
+        static const char *const kJoinerTableTitles[] = {"ID", "PSKd", "Expiration"};
+
+        static const uint8_t kJoinerTableColumnWidths[] = {
+            23,
+            34,
+            12,
+        };
+
+        OutputTableHeader(kJoinerTableTitles, kJoinerTableColumnWidths);
+
+        while (otCommissionerGetNextJoinerInfo(GetInstancePtr(), &iter, &joinerInfo) == OT_ERROR_NONE)
+        {
+            switch (joinerInfo.mType)
+            {
+            case OT_JOINER_INFO_TYPE_ANY:
+                OutputFormat("| %21s", "*");
+                break;
+
+            case OT_JOINER_INFO_TYPE_EUI64:
+                OutputFormat("|      ");
+                OutputExtAddress(joinerInfo.mSharedId.mEui64);
+                break;
+
+            case OT_JOINER_INFO_TYPE_DISCERNER:
+                OutputFormat("| 0x%016llx/%2u", static_cast<unsigned long long>(joinerInfo.mSharedId.mDiscerner.mValue),
+                             joinerInfo.mSharedId.mDiscerner.mLength);
+                break;
+            }
+
+            OutputFormat(" | %32s | %10d |", joinerInfo.mPskd.m8, joinerInfo.mExpirationTime);
+            OutputLine("");
+        }
+
+        ExitNow(error = OT_ERROR_NONE);
+    }
+
     VerifyOrExit(!aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
 
     memset(&discerner, 0, sizeof(discerner));