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

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


The following commit(s) were added to refs/heads/main by this push:
     new 5b1f7ea6e68 SOLR-18443 Refactor CLI Tools to be independent of 
commons-cli (#4695)
5b1f7ea6e68 is described below

commit 5b1f7ea6e68f57ca21dd6607d31c28face6dd5f2
Author: Jan Høydahl <[email protected]>
AuthorDate: Sat Sep 12 16:09:07 2026 +0200

    SOLR-18443 Refactor CLI Tools to be independent of commons-cli (#4695)
---
 .../core/src/java/org/apache/solr/cli/ApiTool.java |  14 +-
 .../src/java/org/apache/solr/cli/AssertTool.java   | 136 ++++++++---
 .../src/java/org/apache/solr/cli/ClusterTool.java  |  17 +-
 .../src/java/org/apache/solr/cli/ConfigTool.java   |  33 ++-
 .../src/java/org/apache/solr/cli/ExportTool.java   |  40 ++-
 .../java/org/apache/solr/cli/HealthcheckTool.java  |  25 +-
 .../src/java/org/apache/solr/cli/PackageTool.java  | 270 ++++++++++++---------
 .../src/java/org/apache/solr/cli/PostLogsTool.java |  16 +-
 .../src/java/org/apache/solr/cli/PostTool.java     | 114 ++++++---
 .../java/org/apache/solr/cli/RunExampleTool.java   | 205 ++++++++++------
 .../org/apache/solr/cli/SnapshotCreateTool.java    |  20 +-
 .../org/apache/solr/cli/SnapshotDeleteTool.java    |  20 +-
 .../org/apache/solr/cli/SnapshotDescribeTool.java  |  20 +-
 .../org/apache/solr/cli/SnapshotExportTool.java    |  33 ++-
 .../java/org/apache/solr/cli/SnapshotListTool.java |  17 +-
 .../src/java/org/apache/solr/cli/StreamTool.java   | 153 ++++++++----
 .../test/org/apache/solr/cli/StreamToolTest.java   |   3 +-
 17 files changed, 770 insertions(+), 366 deletions(-)

diff --git a/solr/core/src/java/org/apache/solr/cli/ApiTool.java 
b/solr/core/src/java/org/apache/solr/cli/ApiTool.java
index 4a9c86fb848..b04d143acd6 100644
--- a/solr/core/src/java/org/apache/solr/cli/ApiTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/ApiTool.java
@@ -45,6 +45,9 @@ public class ApiTool extends ToolBase {
           .desc("Send a GET request to a Solr API endpoint.")
           .get();
 
+  /** Parameters for the api command, independent of the command line parser. 
*/
+  record ApiParams(String getUrl, String credentials) {}
+
   public ApiTool(ToolRuntime runtime) {
     super(runtime);
   }
@@ -63,8 +66,15 @@ public class ApiTool extends ToolBase {
 
   @Override
   public void runImpl(CommandLine cli) throws Exception {
-    String getUrl = cli.getOptionValue(SOLR_URL_OPTION);
-    String response = callGet(getUrl, 
cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION));
+    ApiParams params =
+        new ApiParams(
+            cli.getOptionValue(SOLR_URL_OPTION),
+            cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION));
+    callApi(params);
+  }
+
+  void callApi(ApiParams params) throws Exception {
+    String response = callGet(params.getUrl(), params.credentials());
 
     // pretty-print the response to stdout
     echo(response);
diff --git a/solr/core/src/java/org/apache/solr/cli/AssertTool.java 
b/solr/core/src/java/org/apache/solr/cli/AssertTool.java
index 928065f0a79..0bc56bd2209 100644
--- a/solr/core/src/java/org/apache/solr/cli/AssertTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/AssertTool.java
@@ -21,6 +21,7 @@ import java.lang.invoke.MethodHandles;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.attribute.FileOwnerAttributeView;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
 import org.apache.commons.cli.CommandLine;
@@ -40,9 +41,9 @@ import org.slf4j.LoggerFactory;
  */
 public class AssertTool extends ToolBase {
   private static final Logger log = 
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
-  private static String message = null;
-  private static boolean useExitCode = false;
-  private static Long timeoutMs = 1000L;
+  private String message = null;
+  private boolean useExitCode = false;
+  private Long timeoutMs = 1000L;
 
   private static final Option IS_NOT_ROOT_OPTION =
       Option.builder().desc("Asserts that we are NOT the root 
user.").longOpt("not-root").get();
@@ -143,6 +144,50 @@ public class AssertTool extends ToolBase {
           .longOpt("exitcode")
           .get();
 
+  /** One requested assertion. Multiple assertions may be requested in a 
single invocation. */
+  sealed interface Assertion {
+    /** Asserts that we are the root user. */
+    record RootUser() implements Assertion {}
+
+    /** Asserts that we are NOT the root user. */
+    record NotRootUser() implements Assertion {}
+
+    /** Asserts that the directory exists. */
+    record DirExists(String dir) implements Assertion {}
+
+    /** Asserts that the directory does NOT exist. */
+    record DirNotExists(String dir) implements Assertion {}
+
+    /** Asserts that we run as the same user that owns the directory. */
+    record SameUser(String dir) implements Assertion {}
+
+    /** Asserts that Solr is running on the given URL. */
+    record SolrRunning(String url) implements Assertion {}
+
+    /** Asserts that Solr is NOT running on the given URL. */
+    record SolrNotRunning(String url) implements Assertion {}
+
+    /** Asserts that Solr on the given URL is running in cloud mode. */
+    record CloudMode(String url) implements Assertion {}
+
+    /** Asserts that Solr on the given URL is NOT running in cloud mode. */
+    record NotCloudMode(String url) implements Assertion {}
+  }
+
+  /**
+   * Parameters for the assert command, independent of the command line 
parser. URL values are the
+   * raw user input; they are normalized when the assertion runs.
+   *
+   * @param credentials credentials used by the URL-based assertions, or null
+   * @param assertions assertions to run, in order
+   */
+  record AssertParams(
+      String message,
+      Long timeoutMs,
+      boolean useExitCode,
+      String credentials,
+      List<Assertion> assertions) {}
+
   public AssertTool(ToolRuntime runtime) {
     super(runtime);
   }
@@ -210,49 +255,72 @@ public class AssertTool extends ToolBase {
    * @throws Exception if a tool failed, e.g. authentication failure
    */
   protected int runAssert(CommandLine cli) throws Exception {
-    message = cli.getOptionValue(MESSAGE_OPTION);
-    timeoutMs = cli.getParsedOptionValue(TIMEOUT_OPTION, timeoutMs);
-    useExitCode = cli.hasOption(EXIT_CODE_OPTION);
-
-    int ret = 0;
+    List<Assertion> assertions = new ArrayList<>();
     if (cli.hasOption(IS_ROOT_OPTION)) {
-      ret += assertRootUser();
+      assertions.add(new Assertion.RootUser());
     }
     if (cli.hasOption(IS_NOT_ROOT_OPTION)) {
-      ret += assertNotRootUser();
+      assertions.add(new Assertion.NotRootUser());
     }
     if (cli.hasOption(DIRECTORY_EXISTS_OPTION)) {
-      ret += assertFileExists(cli.getOptionValue(DIRECTORY_EXISTS_OPTION));
+      assertions.add(new 
Assertion.DirExists(cli.getOptionValue(DIRECTORY_EXISTS_OPTION)));
     }
     if (cli.hasOption(DIRECTORY_NOT_EXISTS_OPTION)) {
-      ret += 
assertFileNotExists(cli.getOptionValue(DIRECTORY_NOT_EXISTS_OPTION));
+      assertions.add(new 
Assertion.DirNotExists(cli.getOptionValue(DIRECTORY_NOT_EXISTS_OPTION)));
     }
     if (cli.hasOption(SAME_USER_OPTION)) {
-      ret += sameUser(cli.getOptionValue(SAME_USER_OPTION));
+      assertions.add(new 
Assertion.SameUser(cli.getOptionValue(SAME_USER_OPTION)));
     }
     if (cli.hasOption(IS_RUNNING_ON_OPTION)) {
-      ret +=
-          assertSolrRunning(
-              cli.getOptionValue(IS_RUNNING_ON_OPTION),
-              cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION));
+      assertions.add(new 
Assertion.SolrRunning(cli.getOptionValue(IS_RUNNING_ON_OPTION)));
     }
     if (cli.hasOption(IS_NOT_RUNNING_ON_OPTION)) {
-      ret +=
-          assertSolrNotRunning(
-              cli.getOptionValue(IS_NOT_RUNNING_ON_OPTION),
-              cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION));
+      assertions.add(new 
Assertion.SolrNotRunning(cli.getOptionValue(IS_NOT_RUNNING_ON_OPTION)));
     }
     if (cli.hasOption(IS_CLOUD_OPTION)) {
-      ret +=
-          assertSolrRunningInCloudMode(
-              CLIUtils.normalizeSolrUrl(cli.getOptionValue(IS_CLOUD_OPTION)),
-              cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION));
+      assertions.add(new 
Assertion.CloudMode(cli.getOptionValue(IS_CLOUD_OPTION)));
     }
     if (cli.hasOption(IS_NOT_CLOUD_OPTION)) {
+      assertions.add(new 
Assertion.NotCloudMode(cli.getOptionValue(IS_NOT_CLOUD_OPTION)));
+    }
+    return runAssert(
+        new AssertParams(
+            cli.getOptionValue(MESSAGE_OPTION),
+            cli.getParsedOptionValue(TIMEOUT_OPTION, timeoutMs),
+            cli.hasOption(EXIT_CODE_OPTION),
+            cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION),
+            List.copyOf(assertions)));
+  }
+
+  /**
+   * Runs the requested assertions.
+   *
+   * @return 0 on success, or the number of assertions that failed
+   * @throws Exception if an assertion failed and exit codes are not used, 
e.g. authentication
+   *     failure
+   */
+  int runAssert(AssertParams params) throws Exception {
+    message = params.message();
+    timeoutMs = params.timeoutMs();
+    useExitCode = params.useExitCode();
+    String credentials = params.credentials();
+
+    int ret = 0;
+    for (Assertion assertion : params.assertions()) {
       ret +=
-          assertSolrNotRunningInCloudMode(
-              
CLIUtils.normalizeSolrUrl(cli.getOptionValue(IS_NOT_CLOUD_OPTION)),
-              cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION));
+          switch (assertion) {
+            case Assertion.RootUser() -> assertRootUser();
+            case Assertion.NotRootUser() -> assertNotRootUser();
+            case Assertion.DirExists(String dir) -> assertFileExists(dir);
+            case Assertion.DirNotExists(String dir) -> 
assertFileNotExists(dir);
+            case Assertion.SameUser(String dir) -> sameUser(dir);
+            case Assertion.SolrRunning(String url) -> assertSolrRunning(url, 
credentials);
+            case Assertion.SolrNotRunning(String url) -> 
assertSolrNotRunning(url, credentials);
+            case Assertion.CloudMode(String url) ->
+                assertSolrRunningInCloudMode(CLIUtils.normalizeSolrUrl(url), 
credentials);
+            case Assertion.NotCloudMode(String url) ->
+                
assertSolrNotRunningInCloudMode(CLIUtils.normalizeSolrUrl(url), credentials);
+          };
     }
     return ret;
   }
@@ -344,7 +412,7 @@ public class AssertTool extends ToolBase {
     return 0;
   }
 
-  public static int sameUser(String directory) throws Exception {
+  public int sameUser(String directory) throws Exception {
     Path path = Path.of(directory);
     if (Files.exists(path)) {
       String userForDir = userForDir(path);
@@ -357,28 +425,28 @@ public class AssertTool extends ToolBase {
     return 0;
   }
 
-  public static int assertFileExists(String directory) throws Exception {
+  public int assertFileExists(String directory) throws Exception {
     if (!Files.exists(Path.of(directory))) {
       return exitOrException("Directory " + directory + " does not exist.");
     }
     return 0;
   }
 
-  public static int assertFileNotExists(String directory) throws Exception {
+  public int assertFileNotExists(String directory) throws Exception {
     if (Files.exists(Path.of(directory))) {
       return exitOrException("Directory " + directory + " should not exist.");
     }
     return 0;
   }
 
-  public static int assertRootUser() throws Exception {
+  public int assertRootUser() throws Exception {
     if (!currentUser().equals("root")) {
       return exitOrException("Must run as root user");
     }
     return 0;
   }
 
-  public static int assertNotRootUser() throws Exception {
+  public int assertNotRootUser() throws Exception {
     if (currentUser().equals("root")) {
       return exitOrException("Not allowed to run as root user");
     }
@@ -399,7 +467,7 @@ public class AssertTool extends ToolBase {
     }
   }
 
-  private static int exitOrException(String msg) throws 
AssertionFailureException {
+  private int exitOrException(String msg) throws AssertionFailureException {
     if (useExitCode) {
       return 1;
     } else {
diff --git a/solr/core/src/java/org/apache/solr/cli/ClusterTool.java 
b/solr/core/src/java/org/apache/solr/cli/ClusterTool.java
index 54626e2b7db..f331725b1e3 100644
--- a/solr/core/src/java/org/apache/solr/cli/ClusterTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/ClusterTool.java
@@ -52,6 +52,9 @@ public class ClusterTool extends ToolBase {
           .desc("Set the property to this value.")
           .get();
 
+  /** Parameters for the cluster command, independent of the command line 
parser. */
+  record ClusterParams(String propertyName, String propertyValue, String 
zkHost) {}
+
   public ClusterTool(ToolRuntime runtime) {
     super(runtime);
   }
@@ -71,10 +74,18 @@ public class ClusterTool extends ToolBase {
 
   @Override
   public void runImpl(CommandLine cli) throws Exception {
+    ClusterParams params =
+        new ClusterParams(
+            cli.getOptionValue(PROPERTY_OPTION),
+            cli.getOptionValue(VALUE_OPTION),
+            CLIUtils.getZkHost(cli));
+    setClusterProperty(params);
+  }
 
-    String propertyName = cli.getOptionValue(PROPERTY_OPTION);
-    String propertyValue = cli.getOptionValue(VALUE_OPTION);
-    String zkHost = CLIUtils.getZkHost(cli);
+  void setClusterProperty(ClusterParams params) throws Exception {
+    String propertyName = params.propertyName();
+    String propertyValue = params.propertyValue();
+    String zkHost = params.zkHost();
 
     if (!ZkController.checkChrootPath(zkHost, true)) {
       throw new IllegalStateException(
diff --git a/solr/core/src/java/org/apache/solr/cli/ConfigTool.java 
b/solr/core/src/java/org/apache/solr/cli/ConfigTool.java
index a4145168165..377cd6a4481 100644
--- a/solr/core/src/java/org/apache/solr/cli/ConfigTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/ConfigTool.java
@@ -72,6 +72,15 @@ public class ConfigTool extends ToolBase {
           .desc("Set the property to this value; accepts JSON objects and 
strings.")
           .get();
 
+  /** Parameters for the config command, independent of the command line 
parser. */
+  record ConfigParams(
+      String solrUrl,
+      String action,
+      String collection,
+      String property,
+      String value,
+      String credentials) {}
+
   public ConfigTool(ToolRuntime runtime) {
     super(runtime);
   }
@@ -96,14 +105,31 @@ public class ConfigTool extends ToolBase {
   public void runImpl(CommandLine cli) throws Exception {
     String solrUrl = CLIUtils.normalizeSolrUrl(cli);
     String action = cli.getOptionValue(ACTION_OPTION, "set-property");
-    String collection = cli.getOptionValue(COLLECTION_NAME_OPTION);
-    String property = cli.getOptionValue(PROPERTY_OPTION);
     String value = cli.getOptionValue(VALUE_OPTION);
 
     // value is required unless the property is one of the "unset-" type.
     if (!action.contains("unset-") && value == null) {
       throw new MissingArgumentException("'value' is a required option.");
     }
+
+    ConfigParams params =
+        new ConfigParams(
+            solrUrl,
+            action,
+            cli.getOptionValue(COLLECTION_NAME_OPTION),
+            cli.getOptionValue(PROPERTY_OPTION),
+            value,
+            cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION));
+    updateConfig(params);
+  }
+
+  void updateConfig(ConfigParams params) throws Exception {
+    String solrUrl = params.solrUrl();
+    String action = params.action();
+    String collection = params.collection();
+    String property = params.property();
+    String value = params.value();
+
     Map<String, Object> jsonObj = new HashMap<>();
     if (value != null) {
       Map<String, String> setMap = new HashMap<>();
@@ -122,8 +148,7 @@ public class ConfigTool extends ToolBase {
     echo("\nPOSTing request to Config API: " + solrUrl + updatePath);
     echoIfVerbose(jsonBody);
 
-    try (SolrClient solrClient =
-        CLIUtils.getSolrClient(solrUrl, 
cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION))) {
+    try (SolrClient solrClient = CLIUtils.getSolrClient(solrUrl, 
params.credentials())) {
       NamedList<Object> result = SolrCLI.postJsonToSolr(solrClient, 
updatePath, jsonBody);
       Integer statusCode = (Integer) result._get(List.of("responseHeader", 
"status"), null);
       if (statusCode == 0) {
diff --git a/solr/core/src/java/org/apache/solr/cli/ExportTool.java 
b/solr/core/src/java/org/apache/solr/cli/ExportTool.java
index e377e37bc99..36b0db4fcb0 100644
--- a/solr/core/src/java/org/apache/solr/cli/ExportTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/ExportTool.java
@@ -138,6 +138,17 @@ public class ExportTool extends ToolBase {
           .desc("Comma separated list of fields to export. By default all 
fields are fetched.")
           .get();
 
+  /** Parameters for the export command, independent of the command line 
parser. */
+  record ExportParams(
+      String url,
+      String credentials,
+      String query,
+      String output,
+      String format,
+      boolean compress,
+      String fields,
+      String limit) {}
+
   public ExportTool(ToolRuntime runtime) {
     super(runtime);
   }
@@ -292,16 +303,25 @@ public class ExportTool extends ToolBase {
       throw new IllegalArgumentException(
           "Must specify a connection target via -s/--solr-connection, 
--solr-url, or --zk-host.");
     }
-    String credentials = 
cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION);
-    Info info = new MultiThreadedRunner(runtime, url, credentials);
-    info.query = cli.getOptionValue(QUERY_OPTION, "*:*");
-
-    info.setOutFormat(
-        cli.getOptionValue(OUTPUT_OPTION),
-        cli.getOptionValue(FORMAT_OPTION),
-        cli.hasOption(COMPRESS_OPTION));
-    info.fields = cli.getOptionValue(FIELDS_OPTION);
-    info.setLimit(cli.getOptionValue(LIMIT_OPTION, "100"));
+    ExportParams params =
+        new ExportParams(
+            url,
+            cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION),
+            cli.getOptionValue(QUERY_OPTION, "*:*"),
+            cli.getOptionValue(OUTPUT_OPTION),
+            cli.getOptionValue(FORMAT_OPTION),
+            cli.hasOption(COMPRESS_OPTION),
+            cli.getOptionValue(FIELDS_OPTION),
+            cli.getOptionValue(LIMIT_OPTION, "100"));
+    export(params);
+  }
+
+  void export(ExportParams params) throws Exception {
+    Info info = new MultiThreadedRunner(runtime, params.url(), 
params.credentials());
+    info.query = params.query();
+    info.setOutFormat(params.output(), params.format(), params.compress());
+    info.fields = params.fields();
+    info.setLimit(params.limit());
     info.exportDocs();
   }
 
diff --git a/solr/core/src/java/org/apache/solr/cli/HealthcheckTool.java 
b/solr/core/src/java/org/apache/solr/cli/HealthcheckTool.java
index 176debbb0b8..11429805028 100644
--- a/solr/core/src/java/org/apache/solr/cli/HealthcheckTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/HealthcheckTool.java
@@ -73,6 +73,9 @@ public class HealthcheckTool extends ToolBase {
     no_leader
   }
 
+  /** Parameters for the healthcheck command, independent of the command line 
parser. */
+  record HealthcheckParams(String collection, String credentials) {}
+
   /** Requests health information about a specific collection in SolrCloud. */
   public HealthcheckTool(ToolRuntime runtime) {
     super(runtime);
@@ -85,13 +88,15 @@ public class HealthcheckTool extends ToolBase {
       CLIO.err("Healthcheck tool only works in Solr Cloud mode.");
       runtime.exit(1);
     }
+    HealthcheckParams params =
+        new HealthcheckParams(
+            cli.getOptionValue(COLLECTION_NAME_OPTION),
+            cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION));
     var builder =
-        new HttpJettySolrClient.Builder()
-            .withOptionalBasicAuthCredentials(
-                cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION));
+        new 
HttpJettySolrClient.Builder().withOptionalBasicAuthCredentials(params.credentials());
     try (var cloudSolrClient = CLIUtils.getCloudSolrClient(solrConnection, 
builder)) {
       echoIfVerbose("Connecting to Solr at " + solrConnection.toString());
-      runCloudTool(cloudSolrClient, cli);
+      runCloudTool(cloudSolrClient, params);
     }
   }
 
@@ -100,8 +105,9 @@ public class HealthcheckTool extends ToolBase {
     return "healthcheck";
   }
 
-  protected void runCloudTool(CloudSolrClient cloudSolrClient, CommandLine 
cli) throws Exception {
-    String collection = cli.getOptionValue(COLLECTION_NAME_OPTION);
+  protected void runCloudTool(CloudSolrClient cloudSolrClient, 
HealthcheckParams params)
+      throws Exception {
+    String collection = params.collection();
 
     log.debug("Running healthcheck for {}", collection);
 
@@ -152,13 +158,10 @@ public class HealthcheckTool extends ToolBase {
           q.setRows(0);
           q.set(DISTRIB, "false");
           try (var solrClientForCollection =
-              CLIUtils.getSolrClient(
-                  coreUrl, 
cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION))) {
+              CLIUtils.getSolrClient(coreUrl, params.credentials())) {
             qr = solrClientForCollection.query(q);
             numDocs = qr.getResults().getNumFound();
-            try (var solrClient =
-                CLIUtils.getSolrClient(
-                    r.getBaseUrl(), 
cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION))) {
+            try (var solrClient = CLIUtils.getSolrClient(r.getBaseUrl(), 
params.credentials())) {
               SystemInfoResponse sysResponse = (new 
SystemInfoRequest()).process(solrClient);
               uptime = SolrCLI.uptime(sysResponse.getJVMUpTimeMillis());
               memory =
diff --git a/solr/core/src/java/org/apache/solr/cli/PackageTool.java 
b/solr/core/src/java/org/apache/solr/cli/PackageTool.java
index aaa3649d1be..72b1c1ed8c4 100644
--- a/solr/core/src/java/org/apache/solr/cli/PackageTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/PackageTool.java
@@ -129,149 +129,67 @@ public class PackageTool extends ToolBase {
 
       String cmd = cli.getArgs()[0];
 
-      try (SolrClient solrClient = CLIUtils.getSolrClient(cli, true)) {
+      try (SolrClient solrClient =
+          CLIUtils.getSolrClient(
+              solrUrl, 
cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION), true)) {
         packageManager = new PackageManager(runtime, solrClient, solrUrl, 
zkHost);
         try {
           repositoryManager = new RepositoryManager(solrClient, 
packageManager);
 
+          // Dispatches to a parser-independent method per sub-command
           switch (cmd) {
             case "add-repo":
-              String repoName = cli.getArgs()[1];
-              String repoUrl = cli.getArgs()[2];
-              repositoryManager.addRepository(repoName, repoUrl);
-              printGreen("Added repository: " + repoName);
+              addRepo(cli.getArgs()[1], cli.getArgs()[2]);
               break;
             case "add-key":
-              String keyFilename = cli.getArgs()[1];
-              Path path = Path.of(keyFilename);
-              repositoryManager.addKey(Files.readAllBytes(path), 
path.getFileName().toString());
+              addKey(Path.of(cli.getArgs()[1]));
               break;
             case "list-installed":
-              printGreen("Installed packages:\n-----");
-              for (SolrPackageInstance pkg : 
packageManager.fetchInstalledPackageInstances()) {
-                printGreen(pkg);
-              }
+              listInstalled();
               break;
             case "list-available":
-              printGreen("Available packages:\n-----");
-              for (SolrPackage pkg : repositoryManager.getPackages()) {
-                printGreen(pkg.name + " \t\t" + pkg.description);
-                for (SolrPackageRelease version : pkg.versions) {
-                  printGreen("\tVersion: " + version.version);
-                }
-              }
+              listAvailable();
               break;
             case "list-deployed":
               if (cli.hasOption(COLLECTION_OPTION)) {
-                String collection = cli.getOptionValue(COLLECTION_OPTION);
-                Map<String, SolrPackageInstance> packages =
-                    packageManager.getPackagesDeployed(collection);
-                printGreen("Packages deployed on " + collection + ":");
-                for (String packageName : packages.keySet()) {
-                  printGreen("\t" + packages.get(packageName));
-                }
+                
listPackagesDeployedOnCollection(cli.getOptionValue(COLLECTION_OPTION));
               } else {
                 // nuance that we use an arg here instead of requiring a 
--package parameter with a
-                // value
-                // in this code path
-                String packageName = cli.getArgs()[1];
-                Map<String, String> deployedCollections =
-                    packageManager.getDeployedCollections(packageName);
-                if (!deployedCollections.isEmpty()) {
-                  printGreen("Collections on which package " + packageName + " 
was deployed:");
-                  for (String collection : deployedCollections.keySet()) {
-                    printGreen(
-                        "\t"
-                            + collection
-                            + "("
-                            + packageName
-                            + ":"
-                            + deployedCollections.get(collection)
-                            + ")");
-                  }
-                } else {
-                  printGreen("Package " + packageName + " not deployed on any 
collection.");
-                }
+                // value in this code path
+                listCollectionsWithPackageDeployed(cli.getArgs()[1]);
               }
               break;
             case "install":
-              {
-                Pair<String, String> parsedVersion = 
parsePackageVersion(cli.getArgList().get(1));
-                String packageName = parsedVersion.first();
-                String version = parsedVersion.second();
-                boolean success = repositoryManager.install(packageName, 
version);
-                if (success) {
-                  printGreen(packageName + " installed.");
-                } else {
-                  printRed(packageName + " installation failed.");
-                }
-                break;
-              }
+              install(cli.getArgList().get(1));
+              break;
             case "deploy":
-              {
-                if (cli.hasOption(CLUSTER_OPTION) || 
cli.hasOption(COLLECTIONS_OPTION)) {
-                  Pair<String, String> parsedVersion = 
parsePackageVersion(cli.getArgList().get(1));
-                  String packageName = parsedVersion.first();
-                  String version = parsedVersion.second();
-                  boolean noPrompt = cli.hasOption(NO_PROMPT_OPTION);
-                  boolean isUpdate = cli.hasOption(UPDATE_OPTION);
-                  String[] collections =
-                      cli.hasOption(COLLECTIONS_OPTION)
-                          ? PackageUtils.validateCollections(
-                              
cli.getOptionValue(COLLECTIONS_OPTION).split(","))
-                          : new String[] {};
-                  String[] parameters = cli.getOptionValues(PARAM_OPTION);
-                  packageManager.deploy(
-                      packageName,
-                      version,
-                      collections,
-                      cli.hasOption(CLUSTER_OPTION),
-                      parameters,
-                      isUpdate,
-                      noPrompt);
-                } else {
-                  printRed(
-                      "Either specify --cluster to deploy cluster level 
plugins or --collections <list-of-collections> to deploy collection level 
plugins");
-                }
-                break;
+              if (cli.hasOption(CLUSTER_OPTION) || 
cli.hasOption(COLLECTIONS_OPTION)) {
+                deploy(
+                    cli.getArgList().get(1),
+                    cli.hasOption(CLUSTER_OPTION),
+                    cli.getOptionValue(COLLECTIONS_OPTION),
+                    cli.getOptionValues(PARAM_OPTION),
+                    cli.hasOption(UPDATE_OPTION),
+                    cli.hasOption(NO_PROMPT_OPTION));
+              } else {
+                printRed(
+                    "Either specify --cluster to deploy cluster level plugins 
or --collections <list-of-collections> to deploy collection level plugins");
               }
+              break;
             case "undeploy":
-              {
-                if (cli.hasOption(CLUSTER_OPTION) || 
cli.hasOption(COLLECTIONS_OPTION)) {
-                  Pair<String, String> parsedVersion = 
parsePackageVersion(cli.getArgList().get(1));
-                  if (parsedVersion.second() != null) {
-                    throw new SolrException(
-                        ErrorCode.BAD_REQUEST,
-                        "Only package name expected, without a version. 
Actual: "
-                            + cli.getArgList().get(1));
-                  }
-                  String packageName = parsedVersion.first();
-                  String[] collections =
-                      cli.hasOption(COLLECTIONS_OPTION)
-                          ? PackageUtils.validateCollections(
-                              
cli.getOptionValue(COLLECTIONS_OPTION).split(","))
-                          : new String[] {};
-                  packageManager.undeploy(packageName, collections, 
cli.hasOption(CLUSTER_OPTION));
-                } else {
-                  printRed(
-                      "Either specify --cluster to undeploy cluster level 
plugins or -collections <list-of-collections> to undeploy collection level 
plugins");
-                }
-                break;
+              if (cli.hasOption(CLUSTER_OPTION) || 
cli.hasOption(COLLECTIONS_OPTION)) {
+                undeploy(
+                    cli.getArgList().get(1),
+                    cli.hasOption(CLUSTER_OPTION),
+                    cli.getOptionValue(COLLECTIONS_OPTION));
+              } else {
+                printRed(
+                    "Either specify --cluster to undeploy cluster level 
plugins or -collections <list-of-collections> to undeploy collection level 
plugins");
               }
+              break;
             case "uninstall":
-              {
-                Pair<String, String> parsedVersion = 
parsePackageVersion(cli.getArgList().get(1));
-                if (parsedVersion.second() == null) {
-                  throw new SolrException(
-                      ErrorCode.BAD_REQUEST,
-                      "Package name and version are both required. Actual: "
-                          + cli.getArgList().get(1));
-                }
-                String packageName = parsedVersion.first();
-                String version = parsedVersion.second();
-                packageManager.uninstall(packageName, version);
-                break;
-              }
+              uninstall(cli.getArgList().get(1));
+              break;
             default:
               throw new RuntimeException("Unrecognized command: " + cmd);
           }
@@ -292,6 +210,122 @@ public class PackageTool extends ToolBase {
     }
   }
 
+  private void addRepo(String repoName, String repoUrl) throws Exception {
+    repositoryManager.addRepository(repoName, repoUrl);
+    printGreen("Added repository: " + repoName);
+  }
+
+  private void addKey(Path keyFile) throws Exception {
+    repositoryManager.addKey(Files.readAllBytes(keyFile), 
keyFile.getFileName().toString());
+  }
+
+  private void listInstalled() throws Exception {
+    printGreen("Installed packages:\n-----");
+    for (SolrPackageInstance pkg : 
packageManager.fetchInstalledPackageInstances()) {
+      printGreen(pkg);
+    }
+  }
+
+  private void listAvailable() throws Exception {
+    printGreen("Available packages:\n-----");
+    for (SolrPackage pkg : repositoryManager.getPackages()) {
+      printGreen(pkg.name + " \t\t" + pkg.description);
+      for (SolrPackageRelease version : pkg.versions) {
+        printGreen("\tVersion: " + version.version);
+      }
+    }
+  }
+
+  private void listPackagesDeployedOnCollection(String collection) {
+    Map<String, SolrPackageInstance> packages = 
packageManager.getPackagesDeployed(collection);
+    printGreen("Packages deployed on " + collection + ":");
+    for (String packageName : packages.keySet()) {
+      printGreen("\t" + packages.get(packageName));
+    }
+  }
+
+  private void listCollectionsWithPackageDeployed(String packageName) {
+    Map<String, String> deployedCollections = 
packageManager.getDeployedCollections(packageName);
+    if (!deployedCollections.isEmpty()) {
+      printGreen("Collections on which package " + packageName + " was 
deployed:");
+      for (String collection : deployedCollections.keySet()) {
+        printGreen(
+            "\t"
+                + collection
+                + "("
+                + packageName
+                + ":"
+                + deployedCollections.get(collection)
+                + ")");
+      }
+    } else {
+      printGreen("Package " + packageName + " not deployed on any 
collection.");
+    }
+  }
+
+  private void install(String packageNameAndVersion) throws Exception {
+    Pair<String, String> parsedVersion = 
parsePackageVersion(packageNameAndVersion);
+    String packageName = parsedVersion.first();
+    String version = parsedVersion.second();
+    boolean success = repositoryManager.install(packageName, version);
+    if (success) {
+      printGreen(packageName + " installed.");
+    } else {
+      printRed(packageName + " installation failed.");
+    }
+  }
+
+  /**
+   * @param collections raw comma-separated value of the --collections option, 
or null
+   */
+  private void deploy(
+      String packageNameAndVersion,
+      boolean cluster,
+      String collections,
+      String[] parameters,
+      boolean isUpdate,
+      boolean noPrompt)
+      throws Exception {
+    Pair<String, String> parsedVersion = 
parsePackageVersion(packageNameAndVersion);
+    String packageName = parsedVersion.first();
+    String version = parsedVersion.second();
+    String[] collectionArray =
+        collections != null
+            ? PackageUtils.validateCollections(collections.split(","))
+            : new String[] {};
+    packageManager.deploy(
+        packageName, version, collectionArray, cluster, parameters, isUpdate, 
noPrompt);
+  }
+
+  /**
+   * @param collections raw comma-separated value of the --collections option, 
or null
+   */
+  private void undeploy(String packageNameAndVersion, boolean cluster, String 
collections)
+      throws Exception {
+    Pair<String, String> parsedVersion = 
parsePackageVersion(packageNameAndVersion);
+    if (parsedVersion.second() != null) {
+      throw new SolrException(
+          ErrorCode.BAD_REQUEST,
+          "Only package name expected, without a version. Actual: " + 
packageNameAndVersion);
+    }
+    String packageName = parsedVersion.first();
+    String[] collectionArray =
+        collections != null
+            ? PackageUtils.validateCollections(collections.split(","))
+            : new String[] {};
+    packageManager.undeploy(packageName, collectionArray, cluster);
+  }
+
+  private void uninstall(String packageNameAndVersion) throws Exception {
+    Pair<String, String> parsedVersion = 
parsePackageVersion(packageNameAndVersion);
+    if (parsedVersion.second() == null) {
+      throw new SolrException(
+          ErrorCode.BAD_REQUEST,
+          "Package name and version are both required. Actual: " + 
packageNameAndVersion);
+    }
+    packageManager.uninstall(parsedVersion.first(), parsedVersion.second());
+  }
+
   @Override
   public String getHeader() {
     StringBuilder sb = new StringBuilder();
diff --git a/solr/core/src/java/org/apache/solr/cli/PostLogsTool.java 
b/solr/core/src/java/org/apache/solr/cli/PostLogsTool.java
index dd83821ea29..04c0ca745d4 100644
--- a/solr/core/src/java/org/apache/solr/cli/PostLogsTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/PostLogsTool.java
@@ -65,6 +65,9 @@ public class PostLogsTool extends ToolBase {
           .desc("All files found at or below the root directory will be 
indexed.")
           .get();
 
+  /** Parameters for the postlogs command, independent of the command line 
parser. */
+  record PostLogsParams(String url, String rootDir, String credentials) {}
+
   public PostLogsTool(ToolRuntime runtime) {
     super(runtime);
   }
@@ -93,9 +96,16 @@ public class PostLogsTool extends ToolBase {
       throw new IllegalArgumentException(
           "Must specify a connection target via -s/--solr-connection, 
--solr-url, or --zk-host.");
     }
-    String rootDir = cli.getOptionValue(ROOT_DIR_OPTION);
-    String credentials = 
cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION);
-    runCommand(url, rootDir, credentials);
+    PostLogsParams params =
+        new PostLogsParams(
+            url,
+            cli.getOptionValue(ROOT_DIR_OPTION),
+            cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION));
+    runCommand(params);
+  }
+
+  void runCommand(PostLogsParams params) throws IOException {
+    runCommand(params.url(), params.rootDir(), params.credentials());
   }
 
   public void runCommand(String baseUrl, String root, String credentials) 
throws IOException {
diff --git a/solr/core/src/java/org/apache/solr/cli/PostTool.java 
b/solr/core/src/java/org/apache/solr/cli/PostTool.java
index 4975033b8d6..b1ed2d7dbf8 100644
--- a/solr/core/src/java/org/apache/solr/cli/PostTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/PostTool.java
@@ -244,6 +244,42 @@ public class PostTool extends ToolBase {
     mimeMap.put("log", "text/plain");
   }
 
+  /**
+   * Options controlling how posted content and the update request are shaped.
+   *
+   * @param type content type given by the user, or null to auto-detect from 
file endings
+   * @param format {@link #FORMAT_SOLR} when the input is Solr-formatted JSON 
commands, else ""
+   * @param params raw URL-encoded {@code key=value} pairs to pass through to 
the update request
+   */
+  record ContentOptions(String type, String format, String params) {}
+
+  /**
+   * Options controlling traversal of directories (files mode) and links (web 
mode).
+   *
+   * @param fileTypes comma-separated file endings to consider
+   * @param delay seconds to pause between posts
+   * @param recursive max recursion depth, 0 to disable
+   */
+  record CrawlOptions(String fileTypes, int delay, int recursive) {}
+
+  /** Index maintenance actions to run after posting completes. */
+  record UpdateOptions(boolean commit, boolean optimize) {}
+
+  /**
+   * Parameters for the post command, independent of the command line parser.
+   *
+   * @param args positional arguments; files, directories, urls or literal 
data depending on mode
+   */
+  record PostToolParams(
+      URI solrUpdateUrl,
+      String mode,
+      boolean dryRun,
+      String credentials,
+      String[] args,
+      ContentOptions content,
+      CrawlOptions crawl,
+      UpdateOptions update) {}
+
   public PostTool(ToolRuntime runtime) {
     super(runtime);
   }
@@ -273,51 +309,61 @@ public class PostTool extends ToolBase {
 
   @Override
   public void runImpl(CommandLine cli) throws Exception {
-    solrUpdateUrl = null;
-    if (CLIUtils.hasConnectionOption(cli)) {
-      String url =
-          CLIUtils.normalizeSolrUrl(cli)
-              + "/solr/"
-              + cli.getOptionValue(COLLECTION_NAME_OPTION)
-              + "/update";
-      solrUpdateUrl = new URI(url);
-
-    } else {
-      String url =
-          CLIUtils.getDefaultSolrUrl()
-              + "/solr/"
-              + cli.getOptionValue(COLLECTION_NAME_OPTION)
-              + "/update";
-      solrUpdateUrl = new URI(url);
-    }
+    String baseUrl =
+        CLIUtils.hasConnectionOption(cli)
+            ? CLIUtils.normalizeSolrUrl(cli)
+            : CLIUtils.getDefaultSolrUrl();
+    URI updateUrl =
+        new URI(baseUrl + "/solr/" + 
cli.getOptionValue(COLLECTION_NAME_OPTION) + "/update");
 
     String mode = cli.getOptionValue(MODE_OPTION, DATA_MODE_FILES);
+    int defaultDelay = (mode.equals((DATA_MODE_WEB)) ? 10 : 0);
 
-    dryRun = cli.hasOption(DRY_RUN_OPTION);
+    PostToolParams postParams =
+        new PostToolParams(
+            updateUrl,
+            mode,
+            cli.hasOption(DRY_RUN_OPTION),
+            cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION),
+            cli.getArgs(),
+            new ContentOptions(
+                cli.getOptionValue(TYPE_OPTION),
+                cli.hasOption(FORMAT_OPTION)
+                    ? FORMAT_SOLR
+                    : "", // i.e not solr formatted json commands
+                cli.getOptionValue(PARAMS_OPTION, "")),
+            new CrawlOptions(
+                cli.getOptionValue(FILE_TYPES_OPTION, 
PostTool.DEFAULT_FILE_TYPES),
+                cli.getParsedOptionValue(DELAY_OPTION, defaultDelay),
+                cli.getParsedOptionValue(RECURSIVE_OPTION, 1)),
+            new UpdateOptions(!cli.hasOption(SKIP_COMMIT_OPTION), 
cli.hasOption(OPTIMIZE_OPTION)));
+    postDocuments(postParams);
+  }
+
+  /** Seeds the tool state from the given parameters and runs the post job. */
+  void postDocuments(PostToolParams postParams) throws Exception {
+    solrUpdateUrl = postParams.solrUpdateUrl();
+    dryRun = postParams.dryRun();
 
-    if (cli.hasOption(TYPE_OPTION)) {
-      type = cli.getOptionValue(TYPE_OPTION);
+    if (postParams.content().type() != null) {
+      type = postParams.content().type();
       // Turn off automatically looking up the mimetype in favour of what is 
passed in.
       auto = false;
     }
-    format =
-        cli.hasOption(FORMAT_OPTION) ? FORMAT_SOLR : ""; // i.e not solr 
formatted json commands
-    fileTypes = cli.getOptionValue(FILE_TYPES_OPTION, 
PostTool.DEFAULT_FILE_TYPES);
-
-    int defaultDelay = (mode.equals((DATA_MODE_WEB)) ? 10 : 0);
-    delay = cli.getParsedOptionValue(DELAY_OPTION, defaultDelay);
-    recursive = cli.getParsedOptionValue(RECURSIVE_OPTION, 1);
+    format = postParams.content().format();
+    params = postParams.content().params();
+    fileTypes = postParams.crawl().fileTypes();
+    delay = postParams.crawl().delay();
+    recursive = postParams.crawl().recursive();
 
     out = isVerbose() ? CLIO.getOutStream() : null;
-    commit = !cli.hasOption(SKIP_COMMIT_OPTION);
-    optimize = cli.hasOption(OPTIMIZE_OPTION);
-
-    credentials = cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION);
-    args = cli.getArgs();
+    commit = postParams.update().commit();
+    optimize = postParams.update().optimize();
 
-    params = cli.getOptionValue(PARAMS_OPTION, "");
+    credentials = postParams.credentials();
+    args = postParams.args();
 
-    execute(mode);
+    execute(postParams.mode());
   }
 
   /**
diff --git a/solr/core/src/java/org/apache/solr/cli/RunExampleTool.java 
b/solr/core/src/java/org/apache/solr/cli/RunExampleTool.java
index dcb33138367..6c0f07b78f7 100644
--- a/solr/core/src/java/org/apache/solr/cli/RunExampleTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/RunExampleTool.java
@@ -188,6 +188,39 @@ public class RunExampleTool extends ToolBase {
   protected String urlScheme;
   private boolean usingPromptInputs = false;
 
+  /**
+   * Parameters consumed when starting a single Solr node via the bin/solr 
script, common to all
+   * example modes.
+   *
+   * @param extraArgs extra arguments to pass on to the {@code bin/solr start} 
command
+   */
+  record StartSolrParams(
+      String example,
+      String host,
+      String memory,
+      String jvmOpts,
+      boolean force,
+      String credentials,
+      String extraArgs) {}
+
+  /**
+   * Parameters for running a single-node example (techproducts, schemaless or 
films), independent
+   * of the command line parser.
+   *
+   * @param zkHost ZooKeeper connection string resolved from option or 
sysprop, or null
+   */
+  record RunExampleParams(boolean isCloudMode, String zkHost, int port, 
StartSolrParams start) {}
+
+  /**
+   * Parameters for running the multi-node cloud example, independent of the 
command line parser.
+   *
+   * @param promptInputs comma-separated prompt answers, or null when 
prompting interactively
+   * @param zkHost ZooKeeper connection string resolved from option or 
sysprop, or null
+   * @param basePort first node port; remaining nodes use basePort+1..+3 
unless prompted otherwise
+   */
+  record CloudExampleParams(
+      boolean noPrompt, String promptInputs, String zkHost, int basePort, 
StartSolrParams start) {}
+
   /** Default constructor used by the framework when running as a command-line 
application. */
   public RunExampleTool(ToolRuntime runtime) {
     this(null, System.in, runtime);
@@ -235,14 +268,82 @@ public class RunExampleTool extends ToolBase {
     this.urlScheme = cli.getOptionValue(URL_SCHEME_OPTION, "http");
     String exampleType = cli.getOptionValue(EXAMPLE_OPTION);
 
-    serverDir = Path.of(cli.getOptionValue(SERVER_DIR_OPTION));
+    initDirs(
+        cli.getOptionValue(SERVER_DIR_OPTION),
+        cli.getOptionValue(SCRIPT_OPTION),
+        cli.getOptionValue(EXAMPLE_DIR_OPTION),
+        cli.getOptionValue(SOLR_HOME_OPTION),
+        exampleType);
+
+    echoIfVerbose(
+        "Running with\nserverDir="
+            + serverDir.toAbsolutePath()
+            + ",\nexampleDir="
+            + exampleDir.toAbsolutePath()
+            + ",\nsolrHomeDir="
+            + solrHomeDir.toAbsolutePath()
+            + "\nscript="
+            + script);
+
+    if (!"cloud".equals(exampleType)
+        && !"techproducts".equals(exampleType)
+        && !"schemaless".equals(exampleType)
+        && !"films".equals(exampleType)) {
+      throw new IllegalArgumentException(
+          "Unsupported example "
+              + exampleType
+              + "! Please choose one of: cloud, schemaless, techproducts, or 
films");
+    }
+
+    StartSolrParams startParams =
+        new StartSolrParams(
+            exampleType,
+            cli.getOptionValue(HOST_OPTION),
+            cli.getOptionValue(MEMORY_OPTION),
+            cli.getOptionValue(JVM_OPTS_OPTION),
+            cli.hasOption(FORCE_OPTION),
+            cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION),
+            readExtraArgs(cli.getArgs()));
+    String zkHost =
+        CLIUtils.getCliOptionOrPropValue(cli, CommonCLIOptions.ZK_HOST_OPTION, 
"zkHost", null);
+    int port =
+        Integer.parseInt(
+            cli.getOptionValue(
+                PORT_OPTION, System.getenv().getOrDefault("SOLR_PORT_LISTEN", 
"8983")));
+
+    if ("cloud".equals(exampleType)) {
+      runCloudExample(
+          new CloudExampleParams(
+              cli.hasOption(NO_PROMPT_OPTION),
+              cli.getOptionValue(PROMPT_INPUTS_OPTION),
+              zkHost,
+              port,
+              startParams));
+    } else {
+      runExample(
+          new RunExampleParams(!cli.hasOption(USER_MANAGED_OPTION), zkHost, 
port, startParams));
+    }
+  }
+
+  /**
+   * Resolves and validates the server, example and Solr home directories plus 
the bin/solr script
+   * from the given raw values, seeding the corresponding tool state.
+   */
+  void initDirs(
+      String serverDirArg,
+      String scriptArg,
+      String exampleDirArg,
+      String solrHomeArg,
+      String exampleType)
+      throws Exception {
+    serverDir = Path.of(serverDirArg);
     if (!Files.isDirectory(serverDir))
       throw new IllegalArgumentException(
           "Value of --server-dir option is invalid! "
               + serverDir.toAbsolutePath()
               + " is not a directory!");
 
-    script = cli.getOptionValue(SCRIPT_OPTION);
+    script = scriptArg;
     if (script != null) {
       if (!Files.isRegularFile(Path.of(script)))
         throw new IllegalArgumentException(
@@ -261,17 +362,15 @@ public class RunExampleTool extends ToolBase {
     }
 
     exampleDir =
-        (cli.hasOption(EXAMPLE_DIR_OPTION))
-            ? Path.of(cli.getOptionValue(EXAMPLE_DIR_OPTION))
-            : serverDir.getParent().resolve("example");
+        (exampleDirArg != null) ? Path.of(exampleDirArg) : 
serverDir.getParent().resolve("example");
     if (!Files.isDirectory(exampleDir))
       throw new IllegalArgumentException(
           "Value of --example-dir option is invalid! "
               + exampleDir.toAbsolutePath()
               + " is not a directory!");
 
-    if (cli.hasOption(SOLR_HOME_OPTION)) {
-      solrHomeDir = Path.of(cli.getOptionValue(SOLR_HOME_OPTION));
+    if (solrHomeArg != null) {
+      solrHomeDir = Path.of(solrHomeArg);
     } else {
       String solrHomeProp = EnvUtils.getProperty("solr.home");
       if (solrHomeProp != null && !solrHomeProp.isEmpty()) {
@@ -288,44 +387,18 @@ public class RunExampleTool extends ToolBase {
           "Value of --solr-home option is invalid! "
               + solrHomeDir.toAbsolutePath()
               + " is not a directory!");
-
-    echoIfVerbose(
-        "Running with\nserverDir="
-            + serverDir.toAbsolutePath()
-            + ",\nexampleDir="
-            + exampleDir.toAbsolutePath()
-            + ",\nsolrHomeDir="
-            + solrHomeDir.toAbsolutePath()
-            + "\nscript="
-            + script);
-
-    if ("cloud".equals(exampleType)) {
-      runCloudExample(cli);
-    } else if ("techproducts".equals(exampleType)
-        || "schemaless".equals(exampleType)
-        || "films".equals(exampleType)) {
-      runExample(cli, exampleType);
-    } else {
-      throw new IllegalArgumentException(
-          "Unsupported example "
-              + exampleType
-              + "! Please choose one of: cloud, schemaless, techproducts, or 
films");
-    }
   }
 
-  protected void runExample(CommandLine cli, String exampleName) throws 
Exception {
+  void runExample(RunExampleParams params) throws Exception {
+    String exampleName = params.start().example();
     String collectionName = "schemaless".equals(exampleName) ? 
"gettingstarted" : exampleName;
     String configSet =
         "techproducts".equals(exampleName) ? "sample_techproducts_configs" : 
"_default";
 
-    boolean isCloudMode = !cli.hasOption(USER_MANAGED_OPTION);
-    String zkHost =
-        CLIUtils.getCliOptionOrPropValue(cli, CommonCLIOptions.ZK_HOST_OPTION, 
"zkHost", null);
-    int port =
-        Integer.parseInt(
-            cli.getOptionValue(
-                PORT_OPTION, System.getenv().getOrDefault("SOLR_PORT_LISTEN", 
"8983")));
-    Map<String, Object> nodeStatus = startSolr(solrHomeDir, isCloudMode, cli, 
port, zkHost, 30);
+    String zkHost = params.zkHost();
+    int port = params.port();
+    Map<String, Object> nodeStatus =
+        startSolr(solrHomeDir, params.isCloudMode(), params.start(), port, 
zkHost, 30);
 
     String solrUrl = CLIUtils.normalizeSolrUrl((String) 
nodeStatus.get("baseUrl"), false);
 
@@ -335,7 +408,7 @@ public class RunExampleTool extends ToolBase {
     boolean cloudMode = nodeStatus.get("cloud") != null;
     if (cloudMode) {
       if (CLIUtils.safeCheckCollectionExists(
-          solrUrl, collectionName, 
cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION))) {
+          solrUrl, collectionName, params.start().credentials())) {
         alreadyExists = true;
         echo(
             "\nWARNING: Collection '"
@@ -344,8 +417,7 @@ public class RunExampleTool extends ToolBase {
       }
     } else {
       String coreName = collectionName;
-      if (CLIUtils.safeCheckCoreExists(
-          solrUrl, coreName, 
cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION))) {
+      if (CLIUtils.safeCheckCoreExists(solrUrl, coreName, 
params.start().credentials())) {
         alreadyExists = true;
         echo(
             "\nWARNING: Core '"
@@ -414,9 +486,7 @@ public class RunExampleTool extends ToolBase {
             "exampledocs directory not found, skipping indexing step for the 
techproducts example");
       }
     } else if ("films".equals(exampleName) && !alreadyExists) {
-      try (SolrClient solrClient =
-          CLIUtils.getSolrClient(
-              solrUrl, 
cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION))) {
+      try (SolrClient solrClient = CLIUtils.getSolrClient(solrUrl, 
params.start().credentials())) {
         echo("Adding dense vector field type to films schema");
         SolrCLI.postJsonToSolr(
             solrClient,
@@ -536,16 +606,13 @@ public class RunExampleTool extends ToolBase {
     }
   }
 
-  protected void runCloudExample(CommandLine cli) throws Exception {
+  void runCloudExample(CloudExampleParams params) throws Exception {
 
-    usingPromptInputs = cli.hasOption(PROMPT_INPUTS_OPTION);
-    boolean prompt = !cli.hasOption(NO_PROMPT_OPTION);
+    usingPromptInputs = params.promptInputs() != null;
+    boolean prompt = !params.noPrompt();
     int numNodes = 2;
     int[] cloudPorts = new int[] {8983, 7574, 8984, 7575};
-    int defaultPort =
-        Integer.parseInt(
-            cli.getOptionValue(
-                PORT_OPTION, System.getenv().getOrDefault("SOLR_PORT_LISTEN", 
"8983")));
+    int defaultPort = params.basePort();
     if (defaultPort != 8983) {
       // Override the old default port numbers if user has started the example 
overriding
       // SOLR_PORT_LISTEN
@@ -557,7 +624,7 @@ public class RunExampleTool extends ToolBase {
     Scanner readInput = null;
     if (usingPromptInputs) {
       // Create a scanner from the provided prompts
-      String promptsValue = cli.getOptionValue(PROMPT_INPUTS_OPTION);
+      String promptsValue = params.promptInputs();
       InputStream promptsStream =
           new 
ByteArrayInputStream(promptsValue.getBytes(StandardCharsets.UTF_8));
       readInput = new Scanner(promptsStream, StandardCharsets.UTF_8);
@@ -622,12 +689,11 @@ public class RunExampleTool extends ToolBase {
     }
 
     // deal with extra args passed to the script to run the example
-    String zkHost =
-        CLIUtils.getCliOptionOrPropValue(cli, CommonCLIOptions.ZK_HOST_OPTION, 
"zkHost", null);
+    String zkHost = params.zkHost();
 
     // start the first node (most likely with embedded ZK)
     Map<String, Object> nodeStatus =
-        startSolr(node1Dir.resolve("solr"), true, cli, cloudPorts[0], zkHost, 
30);
+        startSolr(node1Dir.resolve("solr"), true, params.start(), 
cloudPorts[0], zkHost, 30);
 
     if (zkHost == null) {
       @SuppressWarnings("unchecked")
@@ -646,7 +712,7 @@ public class RunExampleTool extends ToolBase {
         startSolr(
             solrHomeDir.resolve("node" + (n + 1)).resolve("solr"),
             true,
-            cli,
+            params.start(),
             cloudPorts[n],
             zkHost,
             30);
@@ -660,11 +726,7 @@ public class RunExampleTool extends ToolBase {
     // create the collection
     String collectionName =
         createCloudExampleCollection(
-            numNodes,
-            readInput,
-            prompt,
-            solrUrl,
-            cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION));
+            numNodes, readInput, prompt, solrUrl, 
params.start().credentials());
 
     echo("\n\nSolrCloud example running, please visit: " + solrUrl + " \n");
   }
@@ -711,28 +773,28 @@ public class RunExampleTool extends ToolBase {
     }
   }
 
-  protected Map<String, Object> startSolr(
+  Map<String, Object> startSolr(
       Path solrHomeDir,
       boolean cloudMode,
-      CommandLine cli,
+      StartSolrParams params,
       int port,
       String zkHost,
       int maxWaitSecs)
       throws Exception {
 
-    String extraArgs = readExtraArgs(cli.getArgs());
+    String extraArgs = params.extraArgs();
 
-    String host = cli.getOptionValue(HOST_OPTION);
-    String memory = cli.getOptionValue(MEMORY_OPTION);
+    String host = params.host();
+    String memory = params.memory();
 
     String hostArg = (host != null && !"localhost".equals(host)) ? " --host " 
+ host : "";
     String zkHostArg = (zkHost != null) ? " -z " + zkHost : "";
     String memArg = (memory != null) ? " -m " + memory : "";
     String cloudModeArg = cloudMode ? "" : "--user-managed";
-    String forceArg = cli.hasOption(FORCE_OPTION) ? " --force" : "";
+    String forceArg = params.force() ? " --force" : "";
     String verboseArg = isVerbose() ? "--verbose" : "";
 
-    String jvmOpts = cli.getOptionValue(JVM_OPTS_OPTION);
+    String jvmOpts = params.jvmOpts();
     String jvmOptsArg =
         (jvmOpts != null && !jvmOpts.isEmpty()) ? " --jvm-opts \"" + jvmOpts + 
"\"" : "";
 
@@ -750,7 +812,7 @@ public class RunExampleTool extends ToolBase {
       solrHome = solrHome.substring(cwdPath.length() + 1);
 
     final var syspropArg =
-        ("techproducts".equals(cli.getOptionValue(EXAMPLE_OPTION)))
+        ("techproducts".equals(params.example()))
             ? "-Dsolr.modules=clustering,extraction,langid,ltr,scripting 
-Dsolr.ltr.enabled=true -Dsolr.clustering.enabled=true"
             : "";
 
@@ -845,8 +907,7 @@ public class RunExampleTool extends ToolBase {
       if (code != 0) throw new Exception("Failed to start Solr using command: 
" + startCmdStr);
     }
 
-    return getNodeStatus(
-        solrUrl, cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION), 
maxWaitSecs);
+    return getNodeStatus(solrUrl, params.credentials(), maxWaitSecs);
   }
 
   protected Map<String, Object> checkPortConflict(
diff --git a/solr/core/src/java/org/apache/solr/cli/SnapshotCreateTool.java 
b/solr/core/src/java/org/apache/solr/cli/SnapshotCreateTool.java
index dfc39bf7cb2..e7abf3ca4a7 100644
--- a/solr/core/src/java/org/apache/solr/cli/SnapshotCreateTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/SnapshotCreateTool.java
@@ -44,6 +44,10 @@ public class SnapshotCreateTool extends ToolBase {
           .desc("Name of the snapshot to produce")
           .get();
 
+  /** Parameters for the snapshot-create command, independent of the command 
line parser. */
+  record SnapshotCreateParams(
+      String solrUrl, String credentials, String collectionName, String 
snapshotName) {}
+
   public SnapshotCreateTool(ToolRuntime runtime) {
     super(runtime);
   }
@@ -64,10 +68,18 @@ public class SnapshotCreateTool extends ToolBase {
 
   @Override
   public void runImpl(CommandLine cli) throws Exception {
-    String snapshotName = cli.getOptionValue(SNAPSHOT_NAME_OPTION);
-    String collectionName = cli.getOptionValue(COLLECTION_NAME_OPTION);
-    try (var solrClient = CLIUtils.getSolrClient(cli)) {
-      createSnapshot(solrClient, collectionName, snapshotName);
+    SnapshotCreateParams params =
+        new SnapshotCreateParams(
+            CLIUtils.normalizeSolrUrl(cli),
+            cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION),
+            cli.getOptionValue(COLLECTION_NAME_OPTION),
+            cli.getOptionValue(SNAPSHOT_NAME_OPTION));
+    createSnapshot(params);
+  }
+
+  void createSnapshot(SnapshotCreateParams params) throws Exception {
+    try (var solrClient = CLIUtils.getSolrClient(params.solrUrl(), 
params.credentials())) {
+      createSnapshot(solrClient, params.collectionName(), 
params.snapshotName());
     }
   }
 
diff --git a/solr/core/src/java/org/apache/solr/cli/SnapshotDeleteTool.java 
b/solr/core/src/java/org/apache/solr/cli/SnapshotDeleteTool.java
index 00b5c3c0197..de912e8e562 100644
--- a/solr/core/src/java/org/apache/solr/cli/SnapshotDeleteTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/SnapshotDeleteTool.java
@@ -44,6 +44,10 @@ public class SnapshotDeleteTool extends ToolBase {
           .desc("Name of the snapshot to delete")
           .get();
 
+  /** Parameters for the snapshot-delete command, independent of the command 
line parser. */
+  record SnapshotDeleteParams(
+      String solrUrl, String credentials, String collectionName, String 
snapshotName) {}
+
   public SnapshotDeleteTool(ToolRuntime runtime) {
     super(runtime);
   }
@@ -64,10 +68,18 @@ public class SnapshotDeleteTool extends ToolBase {
 
   @Override
   public void runImpl(CommandLine cli) throws Exception {
-    String snapshotName = cli.getOptionValue(SNAPSHOT_NAME_OPTION);
-    String collectionName = cli.getOptionValue(COLLECTION_NAME_OPTION);
-    try (var solrClient = CLIUtils.getSolrClient(cli)) {
-      deleteSnapshot(solrClient, collectionName, snapshotName);
+    SnapshotDeleteParams params =
+        new SnapshotDeleteParams(
+            CLIUtils.normalizeSolrUrl(cli),
+            cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION),
+            cli.getOptionValue(COLLECTION_NAME_OPTION),
+            cli.getOptionValue(SNAPSHOT_NAME_OPTION));
+    deleteSnapshot(params);
+  }
+
+  void deleteSnapshot(SnapshotDeleteParams params) throws Exception {
+    try (var solrClient = CLIUtils.getSolrClient(params.solrUrl(), 
params.credentials())) {
+      deleteSnapshot(solrClient, params.collectionName(), 
params.snapshotName());
     }
   }
 
diff --git a/solr/core/src/java/org/apache/solr/cli/SnapshotDescribeTool.java 
b/solr/core/src/java/org/apache/solr/cli/SnapshotDescribeTool.java
index 477ad265e7d..e26139f65b7 100644
--- a/solr/core/src/java/org/apache/solr/cli/SnapshotDescribeTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/SnapshotDescribeTool.java
@@ -54,6 +54,10 @@ public class SnapshotDescribeTool extends ToolBase {
           .desc("Name of the snapshot to describe")
           .get();
 
+  /** Parameters for the snapshot-describe command, independent of the command 
line parser. */
+  record SnapshotDescribeParams(
+      String solrUrl, String credentials, String collectionName, String 
snapshotName) {}
+
   public SnapshotDescribeTool(ToolRuntime runtime) {
     super(runtime);
   }
@@ -77,10 +81,18 @@ public class SnapshotDescribeTool extends ToolBase {
 
   @Override
   public void runImpl(CommandLine cli) throws Exception {
-    String snapshotName = cli.getOptionValue(SNAPSHOT_NAME_OPTION);
-    String collectionName = cli.getOptionValue(COLLECTION_NAME_OPTION);
-    try (var solrClient = CLIUtils.getSolrClient(cli)) {
-      describeSnapshot(solrClient, collectionName, snapshotName);
+    SnapshotDescribeParams params =
+        new SnapshotDescribeParams(
+            CLIUtils.normalizeSolrUrl(cli),
+            cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION),
+            cli.getOptionValue(COLLECTION_NAME_OPTION),
+            cli.getOptionValue(SNAPSHOT_NAME_OPTION));
+    describeSnapshot(params);
+  }
+
+  void describeSnapshot(SnapshotDescribeParams params) throws Exception {
+    try (var solrClient = CLIUtils.getSolrClient(params.solrUrl(), 
params.credentials())) {
+      describeSnapshot(solrClient, params.collectionName(), 
params.snapshotName());
     }
   }
 
diff --git a/solr/core/src/java/org/apache/solr/cli/SnapshotExportTool.java 
b/solr/core/src/java/org/apache/solr/cli/SnapshotExportTool.java
index 8abe486e4c0..c8982f1f4aa 100644
--- a/solr/core/src/java/org/apache/solr/cli/SnapshotExportTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/SnapshotExportTool.java
@@ -81,6 +81,15 @@ public class SnapshotExportTool extends ToolBase {
               "Specifies the async request identifier to be used during 
snapshot export preparation.")
           .get();
 
+  /** Parameters for the snapshot-export command, independent of the command 
line parser. */
+  record SnapshotExportParams(
+      String solrUrl,
+      String credentials,
+      String collectionName,
+      String destDir,
+      String backupRepo,
+      String asyncReqId) {}
+
   public SnapshotExportTool(ToolRuntime runtime) {
     super(runtime);
   }
@@ -110,13 +119,25 @@ public class SnapshotExportTool extends ToolBase {
               + "non-incremental backup format, which was removed in Solr 11; 
this command now "
               + "always backs up the collection's current state. Re-run 
without --snapshot-name.");
     }
-    String collectionName = cli.getOptionValue(COLLECTION_NAME_OPTION);
-    String destDir = cli.getOptionValue(DEST_DIR_OPTION);
-    String backupRepo = cli.getOptionValue(BACKUP_REPO_NAME_OPTION);
-    String asyncReqId = cli.getOptionValue(ASYNC_ID_OPTION);
+    SnapshotExportParams params =
+        new SnapshotExportParams(
+            CLIUtils.normalizeSolrUrl(cli),
+            cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION),
+            cli.getOptionValue(COLLECTION_NAME_OPTION),
+            cli.getOptionValue(DEST_DIR_OPTION),
+            cli.getOptionValue(BACKUP_REPO_NAME_OPTION),
+            cli.getOptionValue(ASYNC_ID_OPTION));
+    exportSnapshot(params);
+  }
 
-    try (var solrClient = CLIUtils.getSolrClient(cli)) {
-      exportSnapshot(solrClient, collectionName, destDir, backupRepo, 
asyncReqId);
+  void exportSnapshot(SnapshotExportParams params) throws Exception {
+    try (var solrClient = CLIUtils.getSolrClient(params.solrUrl(), 
params.credentials())) {
+      exportSnapshot(
+          solrClient,
+          params.collectionName(),
+          params.destDir(),
+          params.backupRepo(),
+          params.asyncReqId());
     }
   }
 
diff --git a/solr/core/src/java/org/apache/solr/cli/SnapshotListTool.java 
b/solr/core/src/java/org/apache/solr/cli/SnapshotListTool.java
index 4501fddc839..c1444be5d5d 100644
--- a/solr/core/src/java/org/apache/solr/cli/SnapshotListTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/SnapshotListTool.java
@@ -37,6 +37,9 @@ public class SnapshotListTool extends ToolBase {
           .desc("Name of collection to list snapshots for.")
           .get();
 
+  /** Parameters for the snapshot-list command, independent of the command 
line parser. */
+  record SnapshotListParams(String solrUrl, String credentials, String 
collectionName) {}
+
   public SnapshotListTool(ToolRuntime runtime) {
     super(runtime);
   }
@@ -56,9 +59,17 @@ public class SnapshotListTool extends ToolBase {
 
   @Override
   public void runImpl(CommandLine cli) throws Exception {
-    String collectionName = cli.getOptionValue(COLLECTION_NAME_OPTION);
-    try (var solrClient = CLIUtils.getSolrClient(cli)) {
-      listSnapshots(solrClient, collectionName);
+    SnapshotListParams params =
+        new SnapshotListParams(
+            CLIUtils.normalizeSolrUrl(cli),
+            cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION),
+            cli.getOptionValue(COLLECTION_NAME_OPTION));
+    listSnapshots(params);
+  }
+
+  void listSnapshots(SnapshotListParams params) throws Exception {
+    try (var solrClient = CLIUtils.getSolrClient(params.solrUrl(), 
params.credentials())) {
+      listSnapshots(solrClient, params.collectionName());
     }
   }
 
diff --git a/solr/core/src/java/org/apache/solr/cli/StreamTool.java 
b/solr/core/src/java/org/apache/solr/cli/StreamTool.java
index 22bcd6c6f15..03e93b7f916 100644
--- a/solr/core/src/java/org/apache/solr/cli/StreamTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/StreamTool.java
@@ -22,7 +22,6 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.LineNumberReader;
-import java.io.Reader;
 import java.io.StringReader;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
@@ -38,6 +37,7 @@ import java.util.Set;
 import org.apache.commons.cli.CommandLine;
 import org.apache.commons.cli.Option;
 import org.apache.commons.cli.Options;
+import org.apache.solr.client.solrj.impl.CloudSolrClient;
 import org.apache.solr.client.solrj.io.SolrClientCache;
 import org.apache.solr.client.solrj.io.Tuple;
 import org.apache.solr.client.solrj.io.comp.StreamComparator;
@@ -59,6 +59,23 @@ import org.apache.solr.handler.CatStream;
 /** Supports stream command in the bin/solr script. */
 public class StreamTool extends ToolBase {
 
+  /**
+   * Parameters for the stream command, independent of the command line parser.
+   *
+   * @param args positional arguments; the first entry is the streaming 
expression or a {@code
+   *     .expr} file, the remaining entries substitute {@code $1}, {@code $2}, 
... parameters
+   * @param fields raw comma-separated value of the --fields option, or null
+   */
+  record StreamParams(
+      String[] args,
+      String execution,
+      String arrayDelimiter,
+      String delimiter,
+      boolean includeHeaders,
+      String fields,
+      String collection,
+      String credentials) {}
+
   public StreamTool(ToolRuntime runtime) {
     super(runtime);
   }
@@ -95,7 +112,7 @@ public class StreamTool extends ToolBase {
               "Name of the specific collection to execute expression on if the 
execution is set to 'remote'. Required for 'remote' execution environment.")
           .get();
 
-  private static final Option FIELDS_OPTION =
+  static final Option FIELDS_OPTION =
       Option.builder()
           .longOpt("fields")
           .argName("FIELDS")
@@ -137,38 +154,57 @@ public class StreamTool extends ToolBase {
   }
 
   @Override
-  @SuppressWarnings({"rawtypes"})
   public void runImpl(CommandLine cli) throws Exception {
+    StreamParams params =
+        new StreamParams(
+            cli.getArgs(),
+            cli.getOptionValue(EXECUTION_OPTION, "remote"),
+            cli.getOptionValue(ARRAY_DELIMITER_OPTION, "|"),
+            cli.getOptionValue(DELIMITER_OPTION, "   "),
+            cli.hasOption(HEADER_OPTION),
+            cli.getOptionValue(FIELDS_OPTION),
+            cli.getOptionValue(COLLECTION_OPTION),
+            cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION));
+
+    String expr = readExpressionFromArgs(params.args());
+    echoIfVerbose("Running Expression: " + expr);
 
-    String expressionArgument = cli.getArgs()[0];
-    String execution = cli.getOptionValue(EXECUTION_OPTION, "remote");
-    String arrayDelimiter = cli.getOptionValue(ARRAY_DELIMITER_OPTION, "|");
-    String delimiter = cli.getOptionValue(DELIMITER_OPTION, "   ");
-    boolean includeHeaders = cli.hasOption(HEADER_OPTION);
-    String[] outputHeaders = getOutputFields(cli);
+    // Validate inputs before opening any connection to Solr.
+    boolean local = params.execution().equalsIgnoreCase("local");
+    validateExpressionArgs(local, params.collection(), expr);
+
+    var solrConnection = CLIUtils.getSolrConnection(cli);
+    String solrUrl = local ? null : CLIUtils.normalizeSolrUrl(cli);
+    if (solrConnection == null) {
+      // No connection option given and none discoverable from a running Solr; 
fall back to the
+      // resolved base URL so expressions that need a Solr connection get a 
usable default.
+      solrConnection =
+          CloudSolrClient.CloudSolrClientConnection.parse(
+              solrUrl != null ? solrUrl : CLIUtils.normalizeSolrUrl(cli));
+    }
 
-    LineNumberReader bufferedReader = null;
-    String expr;
-    try {
-      Reader inputStream =
-          expressionArgument.toLowerCase(Locale.ROOT).endsWith(".expr")
-              ? new InputStreamReader(
-                  new FileInputStream(expressionArgument), 
Charset.defaultCharset())
-              : new StringReader(expressionArgument);
-
-      bufferedReader = new LineNumberReader(inputStream);
-      expr = StreamTool.readExpression(bufferedReader, cli.getArgs());
-      echoIfVerbose("Running Expression: " + expr);
-    } finally {
-      if (bufferedReader != null) {
-        bufferedReader.close();
-      }
+    runStream(params, expr, solrConnection, solrUrl);
+  }
+
+  static String readExpressionFromArgs(String[] args) throws IOException {
+    if (args.length == 0) {
+      throw new IllegalArgumentException(
+          "A streaming expression, or a file containing one (*.expr), must be 
passed after the options.");
     }
+    String expressionArgument = args[0];
+    try (LineNumberReader bufferedReader =
+        new LineNumberReader(
+            expressionArgument.toLowerCase(Locale.ROOT).endsWith(".expr")
+                ? new InputStreamReader(
+                    new FileInputStream(expressionArgument), 
Charset.defaultCharset())
+                : new StringReader(expressionArgument))) {
+      return readExpression(bufferedReader, args);
+    }
+  }
 
-    // Validate inputs before opening any connection to Solr.
-    boolean local = execution.equalsIgnoreCase("local");
+  private static void validateExpressionArgs(boolean local, String collection, 
String expr) {
     if (!local) {
-      if (!cli.hasOption(COLLECTION_OPTION)) {
+      if (collection == null) {
         throw new IllegalStateException(
             "You must provide --name COLLECTION with --execution remote 
parameter.");
       }
@@ -177,16 +213,30 @@ public class StreamTool extends ToolBase {
             "The stdin() expression is only usable with --execution local.");
       }
     }
+  }
+
+  @SuppressWarnings({"rawtypes"})
+  void runStream(
+      StreamParams params,
+      String expr,
+      CloudSolrClient.CloudSolrClientConnection solrConnection,
+      String solrUrl)
+      throws Exception {
+    boolean local = params.execution().equalsIgnoreCase("local");
+    String arrayDelimiter = params.arrayDelimiter();
+    String delimiter = params.delimiter();
+    boolean includeHeaders = params.includeHeaders();
+    String[] outputHeaders = getOutputFields(params.fields());
 
     // a stream needs a context
-    StreamContext streamContext = createStreamContext(cli);
+    StreamContext streamContext = createStreamContext(solrConnection, 
params.credentials());
     // create the stream
     PushBackStream pushBackStream = null;
     try {
       if (local) {
         pushBackStream = doLocalMode(expr, streamContext.getStreamFactory());
       } else {
-        pushBackStream = doRemoteMode(expr, cli);
+        pushBackStream = doRemoteMode(expr, solrUrl, params.collection());
       }
       pushBackStream.setStreamContext(streamContext);
       pushBackStream.open();
@@ -250,9 +300,9 @@ public class StreamTool extends ToolBase {
     echoIfVerbose("StreamTool -- Done.");
   }
 
-  private StreamContext createStreamContext(CommandLine cli) throws Exception {
+  private StreamContext createStreamContext(
+      CloudSolrClient.CloudSolrClientConnection solrConnection, String 
credentials) {
     var jettyClientBuilder = new HttpJettySolrClient.Builder();
-    String credentials = 
cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION);
     jettyClientBuilder.withOptionalBasicAuthCredentials(credentials);
     HttpJettySolrClient client = jettyClientBuilder.build();
 
@@ -267,7 +317,6 @@ public class StreamTool extends ToolBase {
         };
 
     try {
-      var solrConnection = CLIUtils.getSolrConnection(cli);
       echoIfVerbose("Connecting to Solr at " + solrConnection);
 
       StreamContext streamContext = new StreamContext();
@@ -315,15 +364,13 @@ public class StreamTool extends ToolBase {
    * expression is running in a Solr environment.
    *
    * @param expr The streaming expression to be parsed and run remotely
-   * @param cli The CLI invoking the call
+   * @param solrUrl The base URL of the Solr node to send the expression to
+   * @param collection The collection to execute the expression on
    * @return A connection to the streaming expression that receives Tuples as 
they are emitted from
    *     Solr /stream.
    */
-  private PushBackStream doRemoteMode(String expr, CommandLine cli) throws 
Exception {
-
-    String solrUrl = CLIUtils.normalizeSolrUrl(cli);
-    String collection = cli.getOptionValue(COLLECTION_OPTION);
-
+  private PushBackStream doRemoteMode(String expr, String solrUrl, String 
collection)
+      throws Exception {
     return new PushBackStream(
         new SolrStream(solrUrl + "/solr", collection, "/stream", 
params("expr", expr)));
   }
@@ -403,22 +450,22 @@ public class StreamTool extends ToolBase {
     }
   }
 
-  static String[] getOutputFields(CommandLine cli) {
-    if (cli.hasOption(FIELDS_OPTION)) {
-
-      String fl = cli.getOptionValue(FIELDS_OPTION);
-      String[] flArray = fl.split(",");
-      String[] outputHeaders = new String[flArray.length];
-
-      for (int i = 0; i < outputHeaders.length; i++) {
-        outputHeaders[i] = flArray[i].trim();
-      }
-
-      return outputHeaders;
-
-    } else {
+  /**
+   * @param fl raw comma-separated list of fields, or null
+   * @return the trimmed field names, or null if no fields were given
+   */
+  static String[] getOutputFields(String fl) {
+    if (fl == null) {
       return null;
     }
+    String[] flArray = fl.split(",");
+    String[] outputHeaders = new String[flArray.length];
+
+    for (int i = 0; i < outputHeaders.length; i++) {
+      outputHeaders[i] = flArray[i].trim();
+    }
+
+    return outputHeaders;
   }
 
   public static class LocalCatStream extends CatStream {
diff --git a/solr/core/src/test/org/apache/solr/cli/StreamToolTest.java 
b/solr/core/src/test/org/apache/solr/cli/StreamToolTest.java
index a50254ef67e..27ae59cddb9 100644
--- a/solr/core/src/test/org/apache/solr/cli/StreamToolTest.java
+++ b/solr/core/src/test/org/apache/solr/cli/StreamToolTest.java
@@ -78,7 +78,8 @@ public class StreamToolTest extends SolrCloudTestCase {
     ToolRuntime runtime = new CLITestHelper.TestingRuntime(false);
     StreamTool streamTool = new StreamTool(runtime);
     CommandLine cli = SolrCLI.processCommandLineArgs(streamTool, args);
-    String[] outputFields = StreamTool.getOutputFields(cli);
+    String[] outputFields =
+        
StreamTool.getOutputFields(cli.getOptionValue(StreamTool.FIELDS_OPTION));
     assert outputFields != null;
     assertEquals(outputFields.length, 4);
     assertEquals(outputFields[0], "field9");

Reply via email to