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 7d17c6500ef SOLR-18519: Port StreamTool to picocli (#5043)
7d17c6500ef is described below

commit 7d17c6500ef5425419b337a5f82e9da92284e4e3
Author: Serhiy Bzhezytskyy <[email protected]>
AuthorDate: Thu Oct 8 19:11:47 2026 +0300

    SOLR-18519: Port StreamTool to picocli (#5043)
---
 .../SOLR-17697-picocli-experimental-cli.yml        |   2 +
 .../core/src/java/org/apache/solr/cli/SolrCLI.java |   3 +-
 .../src/java/org/apache/solr/cli/StreamTool.java   | 233 ++++++++++++++++++++-
 .../org/apache/solr/cli/StreamToolPicocliTest.java |  97 +++++++++
 .../test/org/apache/solr/cli/StreamToolTest.java   |  44 +++-
 .../modules/deployment-guide/deployment-nav.adoc   |   1 +
 .../modules/deployment-guide/pages/cli/index.adoc  |   1 +
 .../deployment-guide/pages/cli/solr-stream.adoc    | 110 ++++++++++
 8 files changed, 482 insertions(+), 9 deletions(-)

diff --git a/changelog/unreleased/SOLR-17697-picocli-experimental-cli.yml 
b/changelog/unreleased/SOLR-17697-picocli-experimental-cli.yml
index ffdd9051a75..58cd2b4142a 100644
--- a/changelog/unreleased/SOLR-17697-picocli-experimental-cli.yml
+++ b/changelog/unreleased/SOLR-17697-picocli-experimental-cli.yml
@@ -25,3 +25,5 @@ links:
     url: https://issues.apache.org/jira/browse/SOLR-18514
   - name: SOLR-18516
     url: https://issues.apache.org/jira/browse/SOLR-18516
+  - name: SOLR-18519
+    url: https://issues.apache.org/jira/browse/SOLR-18519
diff --git a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java 
b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java
index d8b8e219613..4f2c27d6681 100755
--- a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java
+++ b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java
@@ -88,7 +88,8 @@ import org.slf4j.LoggerFactory;
       ExportTool.class,
       HealthcheckTool.class,
       PostTool.class,
-      PostLogsTool.class
+      PostLogsTool.class,
+      StreamTool.class
     })
 public class SolrCLI implements CLIO, java.util.concurrent.Callable<Integer> {
 
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 da8beec6657..79870742578 100644
--- a/solr/core/src/java/org/apache/solr/cli/StreamTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/StreamTool.java
@@ -37,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.SolrClient;
 import org.apache.solr.client.solrj.impl.CloudSolrClient;
 import org.apache.solr.client.solrj.io.SolrClientCache;
 import org.apache.solr.client.solrj.io.Tuple;
@@ -53,10 +54,25 @@ import 
org.apache.solr.client.solrj.io.stream.expr.StreamFactory;
 import org.apache.solr.client.solrj.jetty.HttpJettySolrClient;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.params.ModifiableSolrParams;
+import org.apache.solr.common.util.EnvUtils;
 import org.apache.solr.common.util.IOUtils;
 import org.apache.solr.handler.CatStream;
 
 /** Supports stream command in the bin/solr script. */
+@SuppressWarnings("UnnecessarilyFullyQualified")
[email protected](
+    name = "stream",
+    description =
+        "The Stream command runs a streaming expression in Solr or in the CLI 
process and prints"
+            + " the results.",
+    footerHeading = "%nExamples:%n",
+    footer = {
+      "  # Run a streaming expression against the techproducts collection",
+      "  bin/solr stream -c techproducts 
'search(techproducts,q=\"iPod\",fl=\"name,price\")'",
+      "",
+      "  # Run a streaming expression stored in a .expr file",
+      "  bin/solr stream --header -c techproducts stream.expr"
+    })
 public class StreamTool extends ToolBase {
 
   /**
@@ -76,6 +92,87 @@ public class StreamTool extends ToolBase {
       String collection,
       String credentials) {}
 
+  // --- picocli fields ---
+  // Named distinctly from the record accessors above where a bare name would 
shadow one.
+
+  @picocli.CommandLine.ArgGroup(exclusive = true, multiplicity = "0..1")
+  private ConnectionOptions connectionOptions;
+
+  @picocli.CommandLine.Mixin private CredentialsOptions credentialsOptions;
+
+  /** Where {@code --execution} runs the expression: in the CLI process or on 
a Solr node. */
+  enum Execution {
+    local,
+    remote
+  }
+
+  /** Accepts the values of {@link Execution} in any case, as the commons-cli 
path does. */
+  static class ExecutionConverter implements 
picocli.CommandLine.ITypeConverter<Execution> {
+    @Override
+    public Execution convert(String value) {
+      try {
+        return Execution.valueOf(value.toLowerCase(Locale.ROOT));
+      } catch (IllegalArgumentException e) {
+        throw new picocli.CommandLine.TypeConversionException(
+            "expected one of " + Arrays.toString(Execution.values()) + " but 
was '" + value + "'");
+      }
+    }
+  }
+
+  @picocli.CommandLine.Option(
+      names = "--execution",
+      defaultValue = "remote",
+      paramLabel = "ENVIRONMENT",
+      converter = ExecutionConverter.class,
+      description =
+          "Where to run the expression, one of: ${COMPLETION-CANDIDATES}. 
'local' is the CLI"
+              + " process, 'remote' is a Solr server. Default is 
'${DEFAULT-VALUE}'.")
+  private Execution executionOpt;
+
+  @picocli.CommandLine.Option(
+      names = {"-c", "--name"},
+      paramLabel = "name",
+      description =
+          "Name of the specific collection to execute expression on if the 
execution is set to"
+              + " 'remote'. Required for 'remote' execution environment.")
+  private String collectionOpt;
+
+  @picocli.CommandLine.Option(
+      names = "--fields",
+      paramLabel = "FIELDS",
+      description =
+          "The fields in the tuples to output. Defaults to fields in the first 
tuple of result"
+              + " set.")
+  private String fieldsOpt;
+
+  @picocli.CommandLine.Option(names = "--header", description = "Specify to 
include a header line.")
+  private boolean headerOpt;
+
+  @picocli.CommandLine.Option(
+      names = "--delimiter",
+      defaultValue = "   ",
+      paramLabel = "CHARACTER",
+      description = "The output delimiter. Default to using three spaces.")
+  private String delimiterOpt;
+
+  @picocli.CommandLine.Option(
+      names = "--array-delimiter",
+      defaultValue = "|",
+      paramLabel = "CHARACTER",
+      description = "The delimiter multi-valued fields. Default to using a 
pipe (|) delimiter.")
+  private String arrayDelimiterOpt;
+
+  @picocli.CommandLine.Parameters(
+      arity = "1..*",
+      paramLabel = "expr",
+      description =
+          "The streaming expression, or a *.expr file, followed by $1/$2/... 
substitution args.")
+  private String[] exprArgs;
+
+  public StreamTool() {
+    this(new DefaultToolRuntime());
+  }
+
   public StreamTool(ToolRuntime runtime) {
     super(runtime);
   }
@@ -94,6 +191,11 @@ public class StreamTool extends ToolBase {
         """;
   }
 
+  /**
+   * @deprecated Only used by the commons-cli parser; the picocli path 
declares this as an annotated
+   *     field.
+   */
+  @Deprecated
   private static final Option EXECUTION_OPTION =
       Option.builder()
           .longOpt("execution")
@@ -103,6 +205,11 @@ public class StreamTool extends ToolBase {
               "Execution environment is either 'local' (i.e CLI process) or 
via a 'remote' Solr server. Default environment is 'remote'.")
           .get();
 
+  /**
+   * @deprecated Only used by the commons-cli parser; the picocli path 
declares this as an annotated
+   *     field.
+   */
+  @Deprecated
   private static final Option COLLECTION_OPTION =
       Option.builder("c")
           .longOpt("name")
@@ -121,9 +228,19 @@ public class StreamTool extends ToolBase {
               "The fields in the tuples to output. Defaults to fields in the 
first tuple of result set.")
           .get();
 
+  /**
+   * @deprecated Only used by the commons-cli parser; the picocli path 
declares this as an annotated
+   *     field.
+   */
+  @Deprecated
   private static final Option HEADER_OPTION =
       Option.builder().longOpt("header").desc("Specify to include a header 
line.").get();
 
+  /**
+   * @deprecated Only used by the commons-cli parser; the picocli path 
declares this as an annotated
+   *     field.
+   */
+  @Deprecated
   private static final Option DELIMITER_OPTION =
       Option.builder()
           .longOpt("delimiter")
@@ -131,6 +248,12 @@ public class StreamTool extends ToolBase {
           .hasArg()
           .desc("The output delimiter. Default to using three spaces.")
           .get();
+
+  /**
+   * @deprecated Only used by the commons-cli parser; the picocli path 
declares this as an annotated
+   *     field.
+   */
+  @Deprecated
   private static final Option ARRAY_DELIMITER_OPTION =
       Option.builder()
           .longOpt("array-delimiter")
@@ -534,7 +657,115 @@ public class StreamTool extends ToolBase {
 
   @Override
   public int callTool() throws Exception {
-    throw new UnsupportedOperationException("This tool does not yet support 
PicoCli");
+    StreamParams params =
+        new StreamParams(
+            exprArgs,
+            executionOpt.name(),
+            arrayDelimiterOpt,
+            delimiterOpt,
+            headerOpt,
+            fieldsOpt,
+            collectionOpt,
+            credentialsOptions.credentials);
+
+    String expr = readExpressionFromArgs(params.args());
+    echoIfVerbose("Running Expression: " + expr);
+
+    // Validate inputs before opening any connection to Solr.
+    boolean local = params.execution().equalsIgnoreCase("local");
+    validateExpressionArgs(local, params.collection(), expr);
+
+    // Mirrors the commons-cli path's call structure exactly, including which 
calls are skipped in
+    // local mode: resolveSolrUrl() is only invoked when actually needed, 
since (like
+    // CLIUtils.normalizeSolrUrl(cli)) it prints a stderr warning when no 
connection option was
+    // given.
+    var solrConnection = resolveSolrConnection();
+    String solrUrl = local ? null : resolveSolrUrl();
+    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 : resolveSolrUrl());
+    }
+
+    runStream(params, expr, solrConnection, solrUrl);
+    return 0;
+  }
+
+  /**
+   * The connection named by {@code --solr-connection} or {@code --zk-host}, 
or by the matching
+   * property when the option is absent, as {@link 
CLIUtils#getSolrConnection(CommandLine)} reads
+   * it; null if neither is set.
+   */
+  private CloudSolrClient.CloudSolrClientConnection connectionFromOptions() 
throws IOException {
+    String solrConnection =
+        (connectionOptions != null && connectionOptions.solrConnection != null)
+            ? connectionOptions.solrConnection
+            : EnvUtils.getProperty("solr.connection");
+    if (solrConnection != null && !solrConnection.isBlank()) {
+      return CloudSolrClient.CloudSolrClientConnection.parse(solrConnection);
+    }
+    String zkHost =
+        (connectionOptions != null && connectionOptions.zkHost != null)
+            ? connectionOptions.zkHost
+            : EnvUtils.getProperty("zkHost");
+    if (zkHost != null && !zkHost.isBlank()) {
+      var zkSolrConnection = 
CloudSolrClient.CloudSolrClientConnection.parse(zkHost);
+      if (!zkSolrConnection.isZookeeper()) {
+        throw new IOException(
+            String.format(
+                Locale.ROOT, "Expected ZooKeeper connection string, but got: 
'%s'.", zkHost));
+      }
+      return zkSolrConnection;
+    }
+    return null;
+  }
+
+  /**
+   * Resolves the base Solr URL used for remote execution and for probing 
whether Solr is running in
+   * cloud mode; mirrors {@link CLIUtils#normalizeSolrUrl(CommandLine)}, 
including its warning when
+   * no connection was given.
+   */
+  private String resolveSolrUrl() throws Exception {
+    if (connectionOptions != null && connectionOptions.solrUrl != null) {
+      return CLIUtils.normalizeSolrUrl(connectionOptions.solrUrl);
+    }
+    var connection = connectionFromOptions();
+    if (connection != null) {
+      return CLIUtils.solrUrlFromConnection(connection, 
credentialsOptions.credentials);
+    }
+    String defaultSolrUrl = CLIUtils.getDefaultSolrUrl();
+    CLIO.err(
+        "Neither --solr-connection, --zk-host or --solr-url parameters, nor 
SOLR_CONNECTION, ZK_HOST env var provided, so assuming solr url is "
+            + defaultSolrUrl
+            + ".");
+    return defaultSolrUrl;
+  }
+
+  /**
+   * Mirrors {@link CLIUtils#getSolrConnection(CommandLine)}: an explicit 
connection wins, otherwise
+   * a running Solr is asked for its ZooKeeper, and null means it is not in 
SolrCloud mode.
+   */
+  private CloudSolrClient.CloudSolrClientConnection resolveSolrConnection() 
throws Exception {
+    var connection = connectionFromOptions();
+    if (connection != null) {
+      return connection;
+    }
+    try (SolrClient solrClient =
+        CLIUtils.getSolrClient(resolveSolrUrl(), 
credentialsOptions.credentials)) {
+      Map<String, Object> status = StatusTool.reportStatus(solrClient);
+      @SuppressWarnings("unchecked")
+      Map<String, Object> cloud = (Map<String, Object>) status.get("cloud");
+      if (cloud == null) {
+        return null;
+      }
+      String zookeeper = (String) cloud.get("ZooKeeper");
+      if (zookeeper.endsWith("(embedded)")) {
+        zookeeper = zookeeper.substring(0, zookeeper.length() - 
"(embedded)".length());
+      }
+      return CloudSolrClient.CloudSolrClientConnection.parse(zookeeper);
+    }
   }
 
   static String readExpression(LineNumberReader bufferedReader, String[] args) 
throws IOException {
diff --git a/solr/core/src/test/org/apache/solr/cli/StreamToolPicocliTest.java 
b/solr/core/src/test/org/apache/solr/cli/StreamToolPicocliTest.java
new file mode 100644
index 00000000000..3dd46ac101d
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/cli/StreamToolPicocliTest.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.cli;
+
+import java.util.Arrays;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.util.SecurityJson;
+import org.junit.Test;
+import picocli.CommandLine;
+
+/**
+ * Runs all {@link StreamToolTest} tests through the picocli invocation path.
+ *
+ * <p>All {@code @Test} methods are inherited; only the invocation strategy is 
overridden.
+ */
+public class StreamToolPicocliTest extends StreamToolTest {
+
+  @Override
+  protected int runTool(String[] args, Class<? extends ToolBase> clazz) throws 
Exception {
+    // args[0] is the tool name used by commons-cli dispatch; strip it for 
picocli.
+    String[] toolArgs = Arrays.copyOfRange(args, 1, args.length);
+    ToolRuntime runtime = new CLITestHelper.TestingRuntime(false);
+    ToolBase tool = 
clazz.getDeclaredConstructor(ToolRuntime.class).newInstance(runtime);
+    return new CommandLine(tool)
+        .setDefaultValueProvider(new CliDefaultValueProvider())
+        .execute(toolArgs);
+  }
+
+  @Test
+  public void testExecutionIsAcceptedInAnyCase() throws Exception {
+    String[] args = {
+      "stream",
+      "--execution",
+      "LOCAL",
+      "-s",
+      getSolrConnection().toString(),
+      "--credentials",
+      SecurityJson.USER_PASS,
+      "echo(Hello)"
+    };
+    assertEquals(0, runTool(args, StreamTool.class));
+  }
+
+  @Test
+  public void testUnknownExecutionIsAUsageError() throws Exception {
+    String[] args = {
+      "stream",
+      "--execution",
+      "elsewhere",
+      "-s",
+      getSolrConnection().toString(),
+      "--credentials",
+      SecurityJson.USER_PASS,
+      "echo(Hello)"
+    };
+    assertEquals(2, runTool(args, StreamTool.class));
+  }
+
+  @Test
+  public void testRunEchoStreamRemotelyUsesSolrConnectionProperty() throws 
Exception {
+    // SOLR_CONNECTION reaches the JVM as the solr.connection property
+    String collectionName = "streamSolrConnectionPropertyCollection";
+    withBasicAuth(CollectionAdminRequest.createCollection(collectionName, 
"_default", 1, 1))
+        .processAndWait(cluster.getSolrClient(), 10);
+    waitForState(
+        "Expected collection to be created with 1 shard and 1 replicas",
+        collectionName,
+        clusterShape(1, 1));
+
+    System.setProperty("solr.connection", getSolrConnection().toString());
+    String[] args = {
+      "stream",
+      "--execution",
+      "remote",
+      "-c",
+      collectionName,
+      "--credentials",
+      SecurityJson.USER_PASS,
+      "echo(Hello)"
+    };
+    assertEquals(0, runTool(args, StreamTool.class));
+  }
+}
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 27ae59cddb9..90d458eb7fd 100644
--- a/solr/core/src/test/org/apache/solr/cli/StreamToolTest.java
+++ b/solr/core/src/test/org/apache/solr/cli/StreamToolTest.java
@@ -43,12 +43,17 @@ import org.junit.Test;
 
 public class StreamToolTest extends SolrCloudTestCase {
 
+  /** Runs the tool. Overridden by the picocli variant of this test. */
+  protected int runTool(String[] args, Class<? extends ToolBase> clazz) throws 
Exception {
+    return CLITestHelper.runTool(args, clazz);
+  }
+
   @BeforeClass
   public static void setupClusterWithSecurityEnabled() throws Exception {
     configureCluster(2).withSecurityJson(SecurityJson.SIMPLE).configure();
   }
 
-  private <T extends SolrRequest<? extends SolrResponse>> T withBasicAuth(T 
req) {
+  <T extends SolrRequest<? extends SolrResponse>> T withBasicAuth(T req) {
     req.setBasicAuthCredentials(SecurityJson.USER, SecurityJson.PASS);
     return req;
   }
@@ -240,7 +245,7 @@ public class StreamToolTest extends SolrCloudTestCase {
           getSolrConnection().toString(),
           expression
         };
-    assertEquals(1, CLITestHelper.runTool(args, StreamTool.class));
+    assertEquals(1, runTool(args, StreamTool.class));
   }
 
   @Test
@@ -259,7 +264,7 @@ public class StreamToolTest extends SolrCloudTestCase {
           SecurityJson.USER_PASS,
           expression
         };
-    assertEquals(0, CLITestHelper.runTool(args, StreamTool.class));
+    assertEquals(0, runTool(args, StreamTool.class));
   }
 
   @Test
@@ -285,7 +290,7 @@ public class StreamToolTest extends SolrCloudTestCase {
       expressionFile.toString()
     };
 
-    assertEquals(0, CLITestHelper.runTool(args, StreamTool.class));
+    assertEquals(0, runTool(args, StreamTool.class));
 
     // test passing in the expression directly
     args =
@@ -301,7 +306,7 @@ public class StreamToolTest extends SolrCloudTestCase {
           expression
         };
 
-    assertEquals(0, CLITestHelper.runTool(args, StreamTool.class));
+    assertEquals(0, runTool(args, StreamTool.class));
   }
 
   @Test
@@ -335,7 +340,7 @@ public class StreamToolTest extends SolrCloudTestCase {
       expressionFile.toString()
     };
 
-    assertEquals(0, CLITestHelper.runTool(args, StreamTool.class));
+    assertEquals(0, runTool(args, StreamTool.class));
 
     // test passing in the expression directly
     args =
@@ -353,7 +358,32 @@ public class StreamToolTest extends SolrCloudTestCase {
           expression
         };
 
-    assertEquals(0, CLITestHelper.runTool(args, StreamTool.class));
+    assertEquals(0, runTool(args, StreamTool.class));
+  }
+
+  @Test
+  public void testRunEchoStreamRemotelyUsesZkHostProperty() throws Exception {
+    String collectionName = "streamZkHostPropertyCollection";
+    withBasicAuth(CollectionAdminRequest.createCollection(collectionName, 
"_default", 1, 1))
+        .processAndWait(cluster.getSolrClient(), 10);
+    waitForState(
+        "Expected collection to be created with 1 shard and 1 replicas",
+        collectionName,
+        clusterShape(1, 1));
+
+    String[] args = {
+      "stream",
+      "--execution",
+      "remote",
+      "-c",
+      collectionName,
+      "--credentials",
+      SecurityJson.USER_PASS,
+      "echo(Hello)"
+    };
+
+    System.setProperty("zkHost", cluster.getZkClient().getZkServerAddress());
+    assertEquals(0, runTool(args, StreamTool.class));
   }
 
   // Copied from StreamExpressionTest.java
diff --git a/solr/solr-ref-guide/modules/deployment-guide/deployment-nav.adoc 
b/solr/solr-ref-guide/modules/deployment-guide/deployment-nav.adoc
index 25397dcfe32..074440d2a99 100644
--- a/solr/solr-ref-guide/modules/deployment-guide/deployment-nav.adoc
+++ b/solr/solr-ref-guide/modules/deployment-guide/deployment-nav.adoc
@@ -34,6 +34,7 @@
 *** xref:cli/solr-start.adoc[start]
 *** xref:cli/solr-status.adoc[status]
 *** xref:cli/solr-stop.adoc[stop]
+*** xref:cli/solr-stream.adoc[stream]
 *** xref:cli/solr-version.adoc[version]
 *** xref:cli/solr-zk.adoc[zk]
 **** xref:cli/solr-zk-cp.adoc[cp]
diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/index.adoc 
b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/index.adoc
index c542dbb975b..e7e950ea5e1 100644
--- a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/index.adoc
+++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/index.adoc
@@ -73,6 +73,7 @@ Any command not listed here is unavailable when the picocli 
implementation is en
 |xref:cli/solr-start.adoc[bin/solr start]
 |xref:cli/solr-status.adoc[bin/solr status]
 |xref:cli/solr-stop.adoc[bin/solr stop]
+|xref:cli/solr-stream.adoc[bin/solr stream]
 |xref:cli/solr-version.adoc[bin/solr version]
 |xref:cli/solr-zk-cp.adoc[bin/solr zk cp]
 |xref:cli/solr-zk-downconfig.adoc[bin/solr zk downconfig]
diff --git 
a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-stream.adoc 
b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-stream.adoc
new file mode 100644
index 00000000000..e62f5b1a1bb
--- /dev/null
+++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-stream.adoc
@@ -0,0 +1,110 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+// DO NOT EDIT -- this page is auto-generated from picocli annotations.
+// To update: modify the @Command/@Option annotations in the Java source, then 
run:
+//   ./gradlew :solr:solr-ref-guide:generateCliDocs
+
+= bin/solr stream
+:page-toclevels: 2
+
+[WARNING]
+====
+This page documents the *experimental* picocli implementation of `bin/solr`, 
which is not enabled by default.
+For what `bin/solr` does by default, see 
xref:solr-control-script-reference.adoc[].
+See xref:cli/index.adoc[] to enable this implementation and for its known 
limitations.
+====
+
+// tag::picocli-generated-man-section-synopsis[]
+== Synopsis
+
+....
+bin/solr stream [-hv] [--header] [--array-delimiter=CHARACTER] [-c=name]
+                [--delimiter=CHARACTER] [--execution=ENVIRONMENT] 
[--fields=FIELDS]
+                [-u=<credentials>] [-s=<solrConnection> | --solr-url=<solrUrl> 
|
+                -z=<zkHost>] expr...
+....
+
+// end::picocli-generated-man-section-synopsis[]
+
+// tag::picocli-generated-man-section-description[]
+== Description
+
+The Stream command runs a streaming expression in Solr or in the CLI process 
and prints the results.
+
+// end::picocli-generated-man-section-description[]
+
+// tag::picocli-generated-man-section-options[]
+== Options
+
+*--array-delimiter*=_CHARACTER_::
+  The delimiter multi-valued fields. Default to using a pipe (|) delimiter.
+
+*-c*, *--name*=_name_::
+  Name of the specific collection to execute expression on if the execution is 
set to 'remote'. Required for 'remote' execution environment.
+
+*--delimiter*=_CHARACTER_::
+  The output delimiter. Default to using three spaces.
+
+*--execution*=_ENVIRONMENT_::
+  Where to run the expression, one of: local, remote. 'local' is the CLI 
process, 'remote' is a Solr server. Default is 'remote'.
+
+*--fields*=_FIELDS_::
+  The fields in the tuples to output. Defaults to fields in the first tuple of 
result set.
+
+*-h*, *--help*::
+  Print this help message and exit.
+
+*--header*::
+  Specify to include a header line.
+
+*-s*, *--solr-connection*=_<solrConnection>_::
+  Zookeeper or HTTP(s) connection string; unnecessary if SOLR_CONNECTION is 
defined in solr.in.sh; otherwise, defaults to localhost:9983.
+
+*--solr-url*=_<solrUrl>_::
+  Base Solr URL, which can be used to determine the zk-host if that's not 
known.
+
+*-u*, *--credentials*=_<credentials>_::
+  Credentials in the format username:password. Example: --credentials 
solr:SolrRocks
+
+*-v*, *--verbose*::
+  Enable verbose mode.
+
+*-z*, *--zk-host*=_<zkHost>_::
+  Zookeeper connection string; unnecessary if ZK_HOST is defined in 
solr.in.sh; otherwise, defaults to localhost:9983.
+
+// end::picocli-generated-man-section-options[]
+
+// tag::picocli-generated-man-section-arguments[]
+== Arguments
+
+_expr_...::
+  The streaming expression, or a *.expr file, followed by $1/$2/... 
substitution args.
+
+// end::picocli-generated-man-section-arguments[]
+
+// tag::picocli-generated-man-section-footer[]
+== Examples
+[%hardbreaks]
+  # Run a streaming expression against the techproducts collection
+  bin/solr stream -c techproducts 
'search(techproducts,q="iPod",fl="name,price")'
+
+[%hardbreaks]
+  # Run a streaming expression stored in a .expr file
+  bin/solr stream --header -c techproducts stream.expr
+
+// end::picocli-generated-man-section-footer[]

Reply via email to