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 5bc0e42134d0..ca1bb3db8fad 100644
--- a/solr/core/src/java/org/apache/solr/cli/ApiTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/ApiTool.java
@@ -34,6 +34,15 @@
*
*
Used to send an arbitrary HTTP request to a Solr API endpoint.
*/
+@SuppressWarnings("UnnecessarilyFullyQualified")
+@picocli.CommandLine.Command(
+ name = "api",
+ description = "Used to send an arbitrary HTTP GET request to a Solr API endpoint.",
+ footerHeading = "%nExamples:%n",
+ footer = {
+ " # Send a GET request to a Solr API endpoint",
+ " bin/solr api -s http://localhost:8983/api/cluster"
+ })
public class ApiTool extends ToolBase {
private static final Option SOLR_URL_OPTION =
@@ -48,6 +57,21 @@ public class ApiTool extends ToolBase {
/** Parameters for the api command, independent of the command line parser. */
record ApiParams(String getUrl, String credentials) {}
+ // --- picocli fields ---
+
+ @picocli.CommandLine.Option(
+ names = {"-s", "--solr-url"},
+ required = true,
+ paramLabel = "URL",
+ description = "Send a GET request to a Solr API endpoint.")
+ private String solrUrlOpt;
+
+ @picocli.CommandLine.Mixin private CredentialsOptions credentialsOptions;
+
+ public ApiTool() {
+ this(new DefaultToolRuntime());
+ }
+
public ApiTool(ToolRuntime runtime) {
super(runtime);
}
@@ -130,6 +154,8 @@ public static ModifiableSolrParams getSolrParamsFromUri(URI uri) {
@Override
public int callTool() throws Exception {
- throw new UnsupportedOperationException("This tool does not yet support PicoCli");
+ ApiParams params = new ApiParams(solrUrlOpt, credentialsOptions.credentials);
+ callApi(params);
+ return 0;
}
}
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 5ba193f9494d..89d7d5614515 100644
--- a/solr/core/src/java/org/apache/solr/cli/AssertTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/AssertTool.java
@@ -39,6 +39,20 @@
* Supports assert command in the bin/solr script. Asserts various conditions and exists with error
* code if there are failures, else continues with no output.
*/
+@SuppressWarnings("UnnecessarilyFullyQualified")
+@picocli.CommandLine.Command(
+ name = "assert",
+ description =
+ "Asserts various conditions and exits with an error code if there are failures, else"
+ + " continues with no output.",
+ footerHeading = "%nExamples:%n",
+ footer = {
+ " # Assert Solr is running before continuing",
+ " bin/solr assert --started http://localhost:8983 --timeout 5000",
+ "",
+ " # Assert we are not running as root",
+ " bin/solr assert --not-root"
+ })
public class AssertTool extends ToolBase {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private String message = null;
@@ -188,6 +202,109 @@ record AssertParams(
String credentials,
List assertions) {}
+ // --- picocli fields ---
+
+ static class RootOptions {
+ @picocli.CommandLine.Option(
+ names = "--root",
+ description = "Asserts that we are the root user.")
+ boolean isRoot;
+
+ @picocli.CommandLine.Option(
+ names = "--not-root",
+ description = "Asserts that we are NOT the root user.")
+ boolean isNotRoot;
+ }
+
+ static class RunningOptions {
+ @picocli.CommandLine.Option(
+ names = "--started",
+ paramLabel = "url",
+ description = "Asserts that Solr is running on a certain URL. Default timeout is 1000ms.")
+ String startedUrl;
+
+ @picocli.CommandLine.Option(
+ names = "--not-started",
+ paramLabel = "url",
+ description =
+ "Asserts that Solr is NOT running on a certain URL. Default timeout is 1000ms.")
+ String notStartedUrl;
+ }
+
+ static class DirectoryOptions {
+ @picocli.CommandLine.Option(
+ names = "--exists",
+ paramLabel = "directory",
+ description = "Asserts that directory exists.")
+ String existsDir;
+
+ @picocli.CommandLine.Option(
+ names = "--not-exists",
+ paramLabel = "directory",
+ description = "Asserts that directory does NOT exist.")
+ String notExistsDir;
+ }
+
+ static class CloudOptions {
+ @picocli.CommandLine.Option(
+ names = "--cloud",
+ paramLabel = "url",
+ description =
+ "Asserts that Solr is running in cloud mode. Also fails if Solr not running. URL"
+ + " should be for root Solr path.")
+ String cloudUrl;
+
+ @picocli.CommandLine.Option(
+ names = "--not-cloud",
+ paramLabel = "url",
+ description =
+ "Asserts that Solr is not running in cloud mode. Also fails if Solr not running. "
+ + " URL should be for root Solr path.")
+ String notCloudUrl;
+ }
+
+ @picocli.CommandLine.ArgGroup(exclusive = true, multiplicity = "0..1")
+ private RootOptions rootOptions;
+
+ @picocli.CommandLine.ArgGroup(exclusive = true, multiplicity = "0..1")
+ private RunningOptions runningOptions;
+
+ @picocli.CommandLine.Option(
+ names = "--same-user",
+ paramLabel = "directory",
+ description = "Asserts that we run as same user that owns .")
+ private String sameUserOpt;
+
+ @picocli.CommandLine.ArgGroup(exclusive = true, multiplicity = "0..1")
+ private DirectoryOptions directoryOptions;
+
+ @picocli.CommandLine.ArgGroup(exclusive = true, multiplicity = "0..1")
+ private CloudOptions cloudOptions;
+
+ @picocli.CommandLine.Option(
+ names = "--message",
+ paramLabel = "message",
+ description = "Exception message to be used in place of the default error message.")
+ private String messageOpt;
+
+ @picocli.CommandLine.Option(
+ names = "--timeout",
+ paramLabel = "ms",
+ defaultValue = "1000",
+ description = "Timeout in ms for commands supporting a timeout.")
+ private long timeoutOpt;
+
+ @picocli.CommandLine.Option(
+ names = "--exitcode",
+ description = "Return an exit code instead of printing error message on assert fail.")
+ private boolean exitCodeOpt;
+
+ @picocli.CommandLine.Mixin private CredentialsOptions credentialsOptions;
+
+ public AssertTool() {
+ this(new DefaultToolRuntime());
+ }
+
public AssertTool(ToolRuntime runtime) {
super(runtime);
}
@@ -496,7 +613,57 @@ private static boolean runningSolrIsCloud(String url, String credentials) throws
@Override
public int callTool() throws Exception {
- throw new UnsupportedOperationException("This tool does not yet support PicoCli");
+ List assertions = new ArrayList<>();
+ if (rootOptions != null && rootOptions.isRoot) {
+ assertions.add(new Assertion.RootUser());
+ }
+ if (rootOptions != null && rootOptions.isNotRoot) {
+ assertions.add(new Assertion.NotRootUser());
+ }
+ if (directoryOptions != null && directoryOptions.existsDir != null) {
+ assertions.add(new Assertion.DirExists(directoryOptions.existsDir));
+ }
+ if (directoryOptions != null && directoryOptions.notExistsDir != null) {
+ assertions.add(new Assertion.DirNotExists(directoryOptions.notExistsDir));
+ }
+ if (sameUserOpt != null) {
+ assertions.add(new Assertion.SameUser(sameUserOpt));
+ }
+ if (runningOptions != null && runningOptions.startedUrl != null) {
+ assertions.add(new Assertion.SolrRunning(runningOptions.startedUrl));
+ }
+ if (runningOptions != null && runningOptions.notStartedUrl != null) {
+ assertions.add(new Assertion.SolrNotRunning(runningOptions.notStartedUrl));
+ }
+ if (cloudOptions != null && cloudOptions.cloudUrl != null) {
+ assertions.add(new Assertion.CloudMode(cloudOptions.cloudUrl));
+ }
+ if (cloudOptions != null && cloudOptions.notCloudUrl != null) {
+ assertions.add(new Assertion.NotCloudMode(cloudOptions.notCloudUrl));
+ }
+
+ try {
+ return runAssert(
+ new AssertParams(
+ messageOpt,
+ timeoutOpt,
+ exitCodeOpt,
+ credentialsOptions.credentials,
+ List.copyOf(assertions)));
+ } catch (Exception exc) {
+ // Mirrors the commons-cli path's runTool() override: an assertion failure or other error
+ // with a message becomes exit code 100, not the ToolBase default of 1.
+ String excMsg = exc.getMessage();
+ if (excMsg == null) {
+ throw exc;
+ }
+ if (isVerbose()) {
+ CLIO.err("\nERROR: " + exc + "\n");
+ } else {
+ CLIO.err("\nERROR: " + excMsg + "\n");
+ }
+ return 100;
+ }
}
public static class AssertionFailureException extends Exception {
diff --git a/solr/core/src/java/org/apache/solr/cli/CLIUtils.java b/solr/core/src/java/org/apache/solr/cli/CLIUtils.java
index 68d4eedc4700..0dde4a85cdc5 100644
--- a/solr/core/src/java/org/apache/solr/cli/CLIUtils.java
+++ b/solr/core/src/java/org/apache/solr/cli/CLIUtils.java
@@ -284,6 +284,77 @@ public static boolean hasConnectionOption(CommandLine cli) {
|| cli.hasOption(CommonCLIOptions.ZK_HOST_OPTION);
}
+ /**
+ * Resolves the base Solr URL for a picocli tool's {@link ConnectionOptions}, honoring whichever
+ * of {@code --solr-url}, {@code --solr-connection} or {@code --zk-host} was given. Mirrors {@link
+ * #normalizeSolrUrl(CommandLine)}, printing the same fallback warning when none was given.
+ */
+ public static String resolveSolrUrl(ConnectionOptions connectionOptions, String credentials)
+ throws Exception {
+ if (connectionOptions != null && connectionOptions.solrUrl != null) {
+ return normalizeSolrUrl(connectionOptions.solrUrl);
+ }
+ String connectionString =
+ connectionOptions != null
+ ? (connectionOptions.solrConnection != null
+ ? connectionOptions.solrConnection
+ : connectionOptions.zkHost)
+ : null;
+ if (connectionString != null) {
+ return solrUrlFromConnection(
+ CloudSolrClient.CloudSolrClientConnection.parse(connectionString), credentials);
+ }
+ String defaultSolrUrl = 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;
+ }
+
+ /**
+ * Resolves a Solr connection from a picocli tool's {@link ConnectionOptions}, mirroring {@link
+ * #getSolrConnection(CommandLine)}: an explicit {@code --solr-connection} or {@code --zk-host}
+ * wins outright, otherwise a running Solr instance (found via {@code --solr-url} or the default
+ * URL) is queried to see if it reports a ZooKeeper connection (SolrCloud mode), returning {@code
+ * null} if it does not.
+ */
+ public static CloudSolrClient.CloudSolrClientConnection resolveSolrConnection(
+ ConnectionOptions connectionOptions, String credentials) throws Exception {
+ if (connectionOptions != null && connectionOptions.solrConnection != null) {
+ return CloudSolrClient.CloudSolrClientConnection.parse(connectionOptions.solrConnection);
+ }
+ if (connectionOptions != null && connectionOptions.zkHost != null) {
+ var zkSolrConnection =
+ CloudSolrClient.CloudSolrClientConnection.parse(connectionOptions.zkHost);
+ if (!zkSolrConnection.isZookeeper()) {
+ throw new IOException(
+ String.format(
+ Locale.ROOT,
+ "Expected ZooKeeper connection string, but got: '%s'.",
+ connectionOptions.zkHost));
+ }
+ return zkSolrConnection;
+ }
+ String resolvedSolrUrl =
+ (connectionOptions != null && connectionOptions.solrUrl != null)
+ ? normalizeSolrUrl(connectionOptions.solrUrl)
+ : getDefaultSolrUrl();
+ try (SolrClient solrClient = getSolrClient(resolvedSolrUrl, credentials)) {
+ Map status = StatusTool.reportStatus(solrClient);
+ @SuppressWarnings("unchecked")
+ Map cloud = (Map) 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);
+ }
+ }
+
/**
* Get the value of the specified CLI option with fallback to system property and default value.
*
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 ce43eff54f1d..bb6b2d8d2448 100644
--- a/solr/core/src/java/org/apache/solr/cli/ClusterTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/ClusterTool.java
@@ -18,6 +18,7 @@
package org.apache.solr.cli;
import java.io.IOException;
+import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.Option;
@@ -32,6 +33,15 @@
*
* Set cluster properties by directly manipulating ZooKeeper.
*/
+@SuppressWarnings("UnnecessarilyFullyQualified")
+@picocli.CommandLine.Command(
+ name = "cluster",
+ description = "Set cluster properties by directly manipulating ZooKeeper.",
+ footerHeading = "%nExamples:%n",
+ footer = {
+ " # Set the urlScheme cluster property",
+ " bin/solr cluster --property urlScheme --value https"
+ })
public class ClusterTool extends ToolBase {
// It is a shame this tool doesn't more closely mimic how the ConfigTool works.
@@ -55,6 +65,33 @@ public class ClusterTool extends ToolBase {
/** Parameters for the cluster command, independent of the command line parser. */
record ClusterParams(String propertyName, String propertyValue, String zkHost) {}
+ // --- picocli fields ---
+
+ @picocli.CommandLine.Option(
+ names = "--property",
+ required = true,
+ paramLabel = "PROPERTY",
+ description = "Name of the Cluster property to apply the action to, such as: 'urlScheme'.")
+ private String propertyOpt;
+
+ @picocli.CommandLine.Option(
+ names = "--value",
+ paramLabel = "VALUE",
+ description = "Set the property to this value.")
+ private String valueOpt;
+
+ @picocli.CommandLine.Option(
+ names = {"-z", "--zk-host"},
+ paramLabel = "zkHost",
+ description =
+ "Zookeeper connection string; unnecessary if ZK_HOST is defined in solr.in.sh;"
+ + " otherwise, discovered from a running Solr instance.")
+ private String zkHostOpt;
+
+ public ClusterTool() {
+ this(new DefaultToolRuntime());
+ }
+
public ClusterTool(ToolRuntime runtime) {
super(runtime);
}
@@ -111,6 +148,32 @@ void setClusterProperty(ClusterParams params) throws Exception {
@Override
public int callTool() throws Exception {
- throw new UnsupportedOperationException("This tool does not yet support PicoCli");
+ ClusterParams params = new ClusterParams(propertyOpt, valueOpt, resolveZkHost());
+ setClusterProperty(params);
+ return 0;
+ }
+
+ /**
+ * Mirrors {@link CLIUtils#getZkHost(CommandLine)}: explicit {@code --zk-host} wins outright,
+ * otherwise discovered from a running Solr instance at the default URL.
+ */
+ private String resolveZkHost() throws Exception {
+ if (zkHostOpt != null && !zkHostOpt.isBlank()) {
+ return zkHostOpt;
+ }
+ String defaultSolrUrl = CLIUtils.getDefaultSolrUrl();
+ try (var solrClient = CLIUtils.getSolrClient(defaultSolrUrl, null)) {
+ Map status = StatusTool.reportStatus(solrClient);
+ @SuppressWarnings("unchecked")
+ Map cloud = (Map) 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 zookeeper;
+ }
}
}
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 4c0ac64935da..97c8869270dc 100644
--- a/solr/core/src/java/org/apache/solr/cli/ConfigTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/ConfigTool.java
@@ -34,6 +34,16 @@
*
* Sends a POST to the Config API to perform a specified action.
*/
+@SuppressWarnings("UnnecessarilyFullyQualified")
+@picocli.CommandLine.Command(
+ name = "config",
+ description = "Sends a POST to the Config API to perform a specified action.",
+ footerHeading = "%nExamples:%n",
+ footer = {
+ " # Set a config property",
+ " bin/solr config -c mycollection --property updateHandler.autoSoftCommit.maxTime --value"
+ + " 10000"
+ })
public class ConfigTool extends ToolBase {
private static final Option COLLECTION_NAME_OPTION =
@@ -81,6 +91,52 @@ record ConfigParams(
String value,
String credentials) {}
+ // --- picocli fields ---
+
+ @picocli.CommandLine.ArgGroup(exclusive = true, multiplicity = "0..1")
+ private ConnectionOptions connectionOptions;
+
+ @picocli.CommandLine.Mixin private CredentialsOptions credentialsOptions;
+
+ @picocli.CommandLine.Option(
+ names = {"-c", "--name"},
+ required = true,
+ paramLabel = "NAME",
+ description = "Name of the collection.")
+ private String nameOpt;
+
+ @picocli.CommandLine.Option(
+ names = {"-a", "--action"},
+ defaultValue = "set-property",
+ paramLabel = "ACTION",
+ description =
+ "Config API action, one of: set-property, unset-property, set-user-property,"
+ + " unset-user-property; default is 'set-property'.")
+ private String actionOpt;
+
+ @picocli.CommandLine.Option(
+ names = "--property",
+ required = true,
+ paramLabel = "PROP",
+ description =
+ "Name of the Config API property to apply the action to, such as:"
+ + " 'updateHandler.autoSoftCommit.maxTime'.")
+ private String propertyOpt;
+
+ // No short "-v": that letter is already ToolBase's --verbose. In the commons-cli path the two
+ // silently collide (VALUE_OPTION is added to the Options set after VERBOSE_OPTION, so its
+ // short form wins and -v currently means --value, not --verbose) - picocli refuses that
+ // collision outright, so --value is long-only here and -v keeps its ToolBase meaning.
+ @picocli.CommandLine.Option(
+ names = "--value",
+ paramLabel = "VALUE",
+ description = "Set the property to this value; accepts JSON objects and strings.")
+ private String valueOpt;
+
+ public ConfigTool() {
+ this(new DefaultToolRuntime());
+ }
+
public ConfigTool(ToolRuntime runtime) {
super(runtime);
}
@@ -165,6 +221,17 @@ void updateConfig(ConfigParams params) throws Exception {
@Override
public int callTool() throws Exception {
- throw new UnsupportedOperationException("This tool does not yet support PicoCli");
+ String solrUrl = CLIUtils.resolveSolrUrl(connectionOptions, credentialsOptions.credentials);
+
+ // value is required unless the property is one of the "unset-" type.
+ if (!actionOpt.contains("unset-") && valueOpt == null) {
+ throw new MissingArgumentException("'value' is a required option.");
+ }
+
+ ConfigParams params =
+ new ConfigParams(
+ solrUrl, actionOpt, nameOpt, propertyOpt, valueOpt, credentialsOptions.credentials);
+ updateConfig(params);
+ return 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 b63c2ba002d1..d5fefce436ee 100644
--- a/solr/core/src/java/org/apache/solr/cli/ExportTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/ExportTool.java
@@ -84,6 +84,15 @@
import org.noggit.JSONWriter;
/** Supports export command in the bin/solr script. */
+@SuppressWarnings("UnnecessarilyFullyQualified")
+@picocli.CommandLine.Command(
+ name = "export",
+ description = "Exports documents from a collection to a local file.",
+ footerHeading = "%nExamples:%n",
+ footer = {
+ " # Export a collection's documents to gettingstarted.json",
+ " bin/solr export -c gettingstarted --solr-url http://localhost:8983"
+ })
public class ExportTool extends ToolBase {
private static final Option COLLECTION_NAME_OPTION =
@@ -149,6 +158,66 @@ record ExportParams(
String fields,
String limit) {}
+ // --- picocli fields ---
+ // The connection group is mandatory (multiplicity "1"): the commons-cli path throws
+ // IllegalArgumentException when no connection target is given, so ArgGroup enforces the same
+ // requirement declaratively.
+
+ @picocli.CommandLine.ArgGroup(exclusive = true, multiplicity = "1")
+ private ConnectionOptions connectionOptions;
+
+ @picocli.CommandLine.Mixin private CredentialsOptions credentialsOptions;
+
+ @picocli.CommandLine.Option(
+ names = {"-c", "--name"},
+ required = true,
+ paramLabel = "NAME",
+ description = "Name of the collection.")
+ private String nameOpt;
+
+ @picocli.CommandLine.Option(
+ names = "--output",
+ paramLabel = "PATH",
+ description =
+ "Path to output the exported data, and optionally the file name, defaults to"
+ + " 'collection-name'.")
+ private String outputOpt;
+
+ @picocli.CommandLine.Option(
+ names = "--format",
+ paramLabel = "FORMAT",
+ description = "Output format for exported docs (json, jsonl or javabin), defaulting to json.")
+ private String formatOpt;
+
+ @picocli.CommandLine.Option(
+ names = "--compress",
+ description = "Compress the output. Defaults to false.")
+ private boolean compressOpt;
+
+ @picocli.CommandLine.Option(
+ names = "--limit",
+ defaultValue = "100",
+ paramLabel = "#",
+ description = "Maximum number of docs to download. Default is 100, use -1 for all docs.")
+ private String limitOpt;
+
+ @picocli.CommandLine.Option(
+ names = "--query",
+ defaultValue = "*:*",
+ paramLabel = "QUERY",
+ description = "A custom query, default is '*:*'.")
+ private String queryOpt;
+
+ @picocli.CommandLine.Option(
+ names = "--fields",
+ paramLabel = "FIELDA,FIELDB",
+ description = "Comma separated list of fields to export. By default all fields are fetched.")
+ private String fieldsOpt;
+
+ public ExportTool() {
+ this(new DefaultToolRuntime());
+ }
+
public ExportTool(ToolRuntime runtime) {
super(runtime);
}
@@ -729,6 +798,20 @@ static long getDocCount(String coreName, SolrClient client, String query)
@Override
public int callTool() throws Exception {
- throw new UnsupportedOperationException("This tool does not yet support PicoCli");
+ String resolvedSolrUrl =
+ CLIUtils.resolveSolrUrl(connectionOptions, credentialsOptions.credentials);
+ String url = resolvedSolrUrl + "/solr/" + nameOpt;
+ ExportParams params =
+ new ExportParams(
+ url,
+ credentialsOptions.credentials,
+ queryOpt,
+ outputOpt,
+ formatOpt,
+ compressOpt,
+ fieldsOpt,
+ limitOpt);
+ export(params);
+ return 0;
}
}
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 7311877cc78c..afe36da1c859 100644
--- a/solr/core/src/java/org/apache/solr/cli/HealthcheckTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/HealthcheckTool.java
@@ -46,6 +46,12 @@
import org.slf4j.LoggerFactory;
/** Supports healthcheck command in the bin/solr script. */
+@SuppressWarnings("UnnecessarilyFullyQualified")
+@picocli.CommandLine.Command(
+ name = "healthcheck",
+ description = "Requests health information about a specific collection in SolrCloud.",
+ footerHeading = "%nExamples:%n",
+ footer = {" # Check the health of a collection", " bin/solr healthcheck -c gettingstarted"})
public class HealthcheckTool extends ToolBase {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
@@ -76,6 +82,24 @@ enum ShardState {
/** Parameters for the healthcheck command, independent of the command line parser. */
record HealthcheckParams(String collection, String credentials) {}
+ // --- picocli fields ---
+
+ @picocli.CommandLine.ArgGroup(exclusive = true, multiplicity = "0..1")
+ private ConnectionOptions connectionOptions;
+
+ @picocli.CommandLine.Mixin private CredentialsOptions credentialsOptions;
+
+ @picocli.CommandLine.Option(
+ names = {"-c", "--name"},
+ required = true,
+ paramLabel = "COLLECTION",
+ description = "Name of the collection to check.")
+ private String nameOpt;
+
+ public HealthcheckTool() {
+ this(new DefaultToolRuntime());
+ }
+
/** Requests health information about a specific collection in SolrCloud. */
public HealthcheckTool(ToolRuntime runtime) {
super(runtime);
@@ -213,7 +237,20 @@ protected void runCloudTool(CloudSolrClient cloudSolrClient, HealthcheckParams p
@Override
public int callTool() throws Exception {
- throw new UnsupportedOperationException("This tool does not yet support PicoCli");
+ var solrConnection =
+ CLIUtils.resolveSolrConnection(connectionOptions, credentialsOptions.credentials);
+ if (solrConnection == null) {
+ CLIO.err("Healthcheck tool only works in Solr Cloud mode.");
+ runtime.exit(1);
+ }
+ HealthcheckParams params = new HealthcheckParams(nameOpt, credentialsOptions.credentials);
+ var builder =
+ new HttpJettySolrClient.Builder().withOptionalBasicAuthCredentials(params.credentials());
+ try (var cloudSolrClient = CLIUtils.getCloudSolrClient(solrConnection, builder)) {
+ echoIfVerbose("Connecting to Solr at " + solrConnection.toString());
+ runCloudTool(cloudSolrClient, params);
+ }
+ return 0;
}
}
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 2764ba4b9fb4..cb0efd81c314 100644
--- a/solr/core/src/java/org/apache/solr/cli/PostLogsTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/PostLogsTool.java
@@ -45,6 +45,16 @@
import org.apache.solr.handler.component.ShardRequest;
/** A command line tool for indexing Solr logs in the out-of-the-box log format. */
+@SuppressWarnings("UnnecessarilyFullyQualified")
+@picocli.CommandLine.Command(
+ name = "postlogs",
+ description = "Indexes Solr logs in the out-of-the-box log format.",
+ footerHeading = "%nExamples:%n",
+ footer = {
+ " # Index all logs found under a directory",
+ " bin/solr postlogs -c gettingstarted --rootdir /var/solr/logs --solr-url"
+ + " http://localhost:8983"
+ })
public class PostLogsTool extends ToolBase {
private static final Option COLLECTION_NAME_OPTION =
@@ -68,6 +78,33 @@ public class PostLogsTool extends ToolBase {
/** Parameters for the postlogs command, independent of the command line parser. */
record PostLogsParams(String url, String rootDir, String credentials) {}
+ // --- picocli fields ---
+ // The connection group is mandatory (multiplicity "1"), mirroring the commons-cli path's
+ // manual IllegalArgumentException when no connection target is given.
+
+ @picocli.CommandLine.ArgGroup(exclusive = true, multiplicity = "1")
+ private ConnectionOptions connectionOptions;
+
+ @picocli.CommandLine.Mixin private CredentialsOptions credentialsOptions;
+
+ @picocli.CommandLine.Option(
+ names = {"-c", "--name"},
+ required = true,
+ paramLabel = "NAME",
+ description = "Name of the collection.")
+ private String nameOpt;
+
+ @picocli.CommandLine.Option(
+ names = "--rootdir",
+ required = true,
+ paramLabel = "DIRECTORY",
+ description = "All files found at or below the root directory will be indexed.")
+ private String rootDirOpt;
+
+ public PostLogsTool() {
+ this(new DefaultToolRuntime());
+ }
+
public PostLogsTool(ToolRuntime runtime) {
super(runtime);
}
@@ -639,6 +676,11 @@ public static String[] getRequestPurposeNames(Integer reqPurpose) {
@Override
public int callTool() throws Exception {
- throw new UnsupportedOperationException("This tool does not yet support PicoCli");
+ String resolvedSolrUrl =
+ CLIUtils.resolveSolrUrl(connectionOptions, credentialsOptions.credentials);
+ String url = resolvedSolrUrl + "/solr/" + nameOpt;
+ PostLogsParams params = new PostLogsParams(url, rootDirOpt, credentialsOptions.credentials);
+ runCommand(params);
+ return 0;
}
}
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 9d85e6fb6798..18f2ceb0966d 100644
--- a/solr/core/src/java/org/apache/solr/cli/PostTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/PostTool.java
@@ -74,6 +74,7 @@
import org.apache.solr.client.api.util.SolrVersion;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.client.solrj.impl.CloudSolrClient;
import org.apache.solr.common.util.Utils;
import org.apache.solr.util.RTimer;
import org.w3c.dom.Document;
@@ -82,6 +83,23 @@
import org.xml.sax.SAXException;
/** Supports post command in the bin/solr script. */
+@SuppressWarnings("UnnecessarilyFullyQualified")
+@picocli.CommandLine.Command(
+ name = "post",
+ description =
+ "Sends one or more files, directories, urls or literal data to Solr for indexing, using"
+ + " the bin/solr CLI.",
+ footerHeading = "%nExamples:%n",
+ footer = {
+ " # Index all JSON files into a collection",
+ " bin/solr post -c gettingstarted *.json",
+ "",
+ " # Index a tab-separated file, using the CSV handler's separator param",
+ " bin/solr post -c gettingstarted --params \"separator=%09\" --type text/csv data.tsv",
+ "",
+ " # Crawl a website one layer deep and index the pages found",
+ " bin/solr post -c gettingstarted --mode web --recursive 1 https://solr.apache.org/"
+ })
public class PostTool extends ToolBase {
public static final String DEFAULT_FILE_TYPES =
@@ -212,6 +230,98 @@ public class PostTool extends ToolBase {
PostTool.PageFetcher pageFetcher = new PostTool.PageFetcher();
+ // --- picocli fields ---
+ // Named distinctly from the commons-cli-era instance fields above (type, recursive, delay,
+ // fileTypes, params, commit, optimize, dryRun, args), which postDocuments() still populates from
+ // whichever parser ran.
+
+ @picocli.CommandLine.ArgGroup(exclusive = true, multiplicity = "0..1")
+ private ConnectionOptions connectionOptions;
+
+ @picocli.CommandLine.Mixin private CredentialsOptions credentialsOptions;
+
+ @picocli.CommandLine.Option(
+ names = {"-c", "--name"},
+ required = true,
+ paramLabel = "name",
+ description = "Name of the collection.")
+ private String nameOpt;
+
+ @picocli.CommandLine.Option(
+ names = "--skip-commit",
+ description = "Do not 'commit', and thus changes won't be visible till a commit occurs.")
+ private boolean skipCommitOpt;
+
+ @picocli.CommandLine.Option(
+ names = {"-o", "--optimize"},
+ description = "Issue an optimize at end of posting documents.")
+ private boolean optimizeOpt;
+
+ @picocli.CommandLine.Option(
+ names = "--mode",
+ defaultValue = DATA_MODE_FILES,
+ paramLabel = "mode",
+ description =
+ "Which mode the Post command is running in, 'files' crawls local directory, 'web'"
+ + " crawls website, 'args' processes input args, and 'stdin' reads a command from"
+ + " standard in. default: files.")
+ private String modeOpt;
+
+ @picocli.CommandLine.Option(
+ names = {"-r", "--recursive"},
+ defaultValue = "1",
+ paramLabel = "recursive",
+ description = "For web crawl, how deep to go. default: 1")
+ private int recursiveOpt;
+
+ @picocli.CommandLine.Option(
+ names = {"-d", "--delay"},
+ paramLabel = "delay",
+ description =
+ "If recursive then delay will be the wait time between posts. default: 10 for web, 0"
+ + " for files")
+ private Integer delayOpt;
+
+ @picocli.CommandLine.Option(
+ names = {"-t", "--type"},
+ paramLabel = "content-type",
+ description = "Specify a specific mimetype to use, such as application/json.")
+ private String typeOpt;
+
+ @picocli.CommandLine.Option(
+ names = {"-ft", "--filetypes"},
+ defaultValue = DEFAULT_FILE_TYPES,
+ paramLabel = "[,,...]",
+ description = "default: " + DEFAULT_FILE_TYPES)
+ private String fileTypesOpt;
+
+ @picocli.CommandLine.Option(
+ names = "--params",
+ defaultValue = "",
+ paramLabel = "=[&=...]",
+ description = "Values must be URL-encoded; these pass through to Solr update request.")
+ private String paramsOpt;
+
+ @picocli.CommandLine.Option(
+ names = "--format",
+ description =
+ "sends application/json content as Solr commands to /update instead of"
+ + " /update/json/docs.")
+ private boolean formatOpt;
+
+ @picocli.CommandLine.Option(
+ names = "--dry-run",
+ description =
+ "Performs a dry run of the posting process without actually sending documents to"
+ + " Solr. Only works with files mode.")
+ private boolean dryRunOpt;
+
+ @picocli.CommandLine.Parameters(
+ arity = "0..*",
+ paramLabel = "FILE",
+ description = "Files, directories, urls or literal data to post, depending on --mode.")
+ private String[] postArgs = new String[0];
+
static {
DATA_MODES.add(DATA_MODE_FILES);
DATA_MODES.add(DATA_MODE_ARGS);
@@ -280,6 +390,10 @@ record PostToolParams(
CrawlOptions crawl,
UpdateOptions update) {}
+ public PostTool() {
+ this(new DefaultToolRuntime());
+ }
+
public PostTool(ToolRuntime runtime) {
super(runtime);
}
@@ -1390,7 +1504,43 @@ protected Set getLinksFromWebPage(URL url, InputStream is, String type, URI
@Override
public int callTool() throws Exception {
- throw new UnsupportedOperationException("This tool does not yet support PicoCli");
+ String resolvedSolrUrl;
+ if (connectionOptions != null && connectionOptions.solrUrl != null) {
+ resolvedSolrUrl = CLIUtils.normalizeSolrUrl(connectionOptions.solrUrl);
+ } else if (connectionOptions != null
+ && (connectionOptions.solrConnection != null || connectionOptions.zkHost != null)) {
+ String connectionString =
+ connectionOptions.solrConnection != null
+ ? connectionOptions.solrConnection
+ : connectionOptions.zkHost;
+ resolvedSolrUrl =
+ CLIUtils.solrUrlFromConnection(
+ CloudSolrClient.CloudSolrClientConnection.parse(connectionString),
+ credentialsOptions.credentials);
+ } else {
+ resolvedSolrUrl = 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 "
+ + resolvedSolrUrl
+ + ".");
+ }
+
+ URI updateUrl = new URI(resolvedSolrUrl + "/solr/" + nameOpt + "/update");
+ int defaultDelay = modeOpt.equals(DATA_MODE_WEB) ? DEFAULT_WEB_DELAY : 0;
+
+ PostToolParams postParams =
+ new PostToolParams(
+ updateUrl,
+ modeOpt,
+ dryRunOpt,
+ credentialsOptions.credentials,
+ postArgs,
+ new ContentOptions(typeOpt, formatOpt ? FORMAT_SOLR : "", paramsOpt),
+ new CrawlOptions(
+ fileTypesOpt, delayOpt != null ? delayOpt : defaultDelay, recursiveOpt),
+ new UpdateOptions(!skipCommitOpt, optimizeOpt));
+ postDocuments(postParams);
+ return 0;
}
/** Utility class to hold the result form a page fetch */
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 b5f0b162fc94..87494c1dfdd3 100644
--- a/solr/core/src/java/org/apache/solr/cli/RunExampleTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/RunExampleTool.java
@@ -56,6 +56,20 @@
* Enhances start command by providing an interactive session with the user to launch (or
* relaunch the -e cloud example)
*/
+@SuppressWarnings("UnnecessarilyFullyQualified")
+@picocli.CommandLine.Command(
+ name = "run_example",
+ description =
+ "Enhances the start command by providing an interactive session with the user to launch"
+ + " (or relaunch) one of the bundled examples.",
+ footerHeading = "%nExamples:%n",
+ footer = {
+ " # Launch the interactive SolrCloud example",
+ " bin/solr start -e cloud",
+ "",
+ " # Launch the techproducts example, accepting all prompt defaults",
+ " bin/solr start -e techproducts --no-prompt"
+ })
public class RunExampleTool extends ToolBase {
private static final String PROMPT_FOR_NUMBER = "Please enter %s [%d]: ";
@@ -221,6 +235,127 @@ record RunExampleParams(boolean isCloudMode, String zkHost, int port, StartSolrP
record CloudExampleParams(
boolean noPrompt, String promptInputs, String zkHost, int basePort, StartSolrParams start) {}
+ // --- picocli fields ---
+
+ @picocli.CommandLine.Option(
+ names = {"-y", "--no-prompt"},
+ description =
+ "Don't prompt for input; accept all defaults when running examples that accept user"
+ + " input.")
+ private boolean noPromptOpt;
+
+ @picocli.CommandLine.Option(
+ names = "--prompt-inputs",
+ paramLabel = "VALUES",
+ description =
+ "Provide comma-separated values for prompts. Same as --no-prompt but uses provided"
+ + " values instead of defaults. Example: --prompt-inputs"
+ + " 3,8983,8984,8985,\"gettingstarted\",2,2,_default")
+ private String promptInputsOpt;
+
+ @picocli.CommandLine.Option(
+ names = {"-e", "--example"},
+ required = true,
+ paramLabel = "NAME",
+ description =
+ "Name of the example to launch, one of: cloud, techproducts, schemaless, films.")
+ private String exampleOpt;
+
+ @picocli.CommandLine.Option(
+ names = "--script",
+ paramLabel = "PATH",
+ description = "Path to the bin/solr script.")
+ private String scriptOpt;
+
+ @picocli.CommandLine.Option(
+ names = {"-d", "--server-dir"},
+ required = true,
+ paramLabel = "DIR",
+ description = "Path to the Solr server directory.")
+ private String serverDirOpt;
+
+ @picocli.CommandLine.Option(
+ names = {"-f", "--force"},
+ description = "Force option in case Solr is run as root.")
+ private boolean forceOpt;
+
+ @picocli.CommandLine.Option(
+ names = "--example-dir",
+ paramLabel = "DIR",
+ description =
+ "Path to the Solr example directory; if not provided, ${serverDir}/../example is"
+ + " expected to exist.")
+ private String exampleDirOpt;
+
+ @picocli.CommandLine.Option(
+ names = "--solr-home",
+ paramLabel = "SOLR_HOME_DIR",
+ description =
+ "Path to the Solr home directory; if not provided, ${serverDir}/solr is expected to"
+ + " exist.")
+ private String solrHomeOpt;
+
+ @picocli.CommandLine.Option(
+ names = "--url-scheme",
+ defaultValue = "http",
+ paramLabel = "SCHEME",
+ description = "Solr URL scheme: http or https, defaults to http if not specified.")
+ private String urlSchemeOpt;
+
+ // No defaultValue attribute: paramLabel "port" matches CliDefaultValueProvider's case
+ // (falls back to the solr.port.listen sysprop / SOLR_PORT_LISTEN env var, else 8983), mirroring
+ // the commons-cli path's System.getenv().getOrDefault("SOLR_PORT_LISTEN", "8983").
+ @picocli.CommandLine.Option(
+ names = {"-p", "--port"},
+ paramLabel = "port",
+ description = "Specify the port to start the Solr HTTP listener on; default is 8983.")
+ private int port;
+
+ @picocli.CommandLine.Option(
+ names = "--host",
+ paramLabel = "HOSTNAME",
+ description = "Specify the hostname for this Solr instance.")
+ private String hostOpt;
+
+ @picocli.CommandLine.Option(
+ names = "--user-managed",
+ description = "Start Solr in User Managed mode.")
+ private boolean userManagedOpt;
+
+ @picocli.CommandLine.Option(
+ names = {"-m", "--memory"},
+ paramLabel = "MEM",
+ description =
+ "Sets the min (-Xms) and max (-Xmx) heap size for the JVM, such as: -m 4g results in:"
+ + " -Xms4g -Xmx4g; by default, this script sets the heap size to 512m.")
+ private String memoryOpt;
+
+ @picocli.CommandLine.Option(
+ names = "--jvm-opts",
+ paramLabel = "OPTS",
+ description =
+ "Additional options to be passed to the JVM when starting example Solr server(s).")
+ private String jvmOptsOpt;
+
+ // paramLabel "zkHost" matches CliDefaultValueProvider's case (falls back to the zkHost
+ // sysprop / ZK_HOST env var, else null), mirroring the commons-cli path's
+ // CLIUtils.getCliOptionOrPropValue(cli, ZK_HOST_OPTION, "zkHost", null).
+ @picocli.CommandLine.Option(
+ names = {"-z", "--zk-host"},
+ paramLabel = "zkHost",
+ description = "Zookeeper connection string.")
+ private String zkHostOpt;
+
+ @picocli.CommandLine.Parameters(
+ arity = "0..*",
+ paramLabel = "ARG",
+ description = "Extra arguments passed through to the underlying bin/solr start command.")
+ private String[] extraArgsOpt = new String[0];
+
+ public RunExampleTool() {
+ this(new DefaultToolRuntime());
+ }
+
/** Default constructor used by the framework when running as a command-line application. */
public RunExampleTool(ToolRuntime runtime) {
this(null, System.in, runtime);
@@ -1140,7 +1275,54 @@ protected void copyIfNeeded(Path src, Path dest) throws IOException {
@Override
public int callTool() throws Exception {
- throw new UnsupportedOperationException("This tool does not yet support PicoCli");
+ if (noPromptOpt && promptInputsOpt != null) {
+ throw new IllegalArgumentException(
+ "Cannot use both --no-prompt and --prompt-inputs options together. "
+ + "Use --no-prompt to accept defaults, or --prompt-inputs to provide specific values.");
+ }
+
+ this.urlScheme = urlSchemeOpt;
+ String exampleType = exampleOpt;
+
+ initDirs(serverDirOpt, scriptOpt, exampleDirOpt, solrHomeOpt, 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,
+ hostOpt,
+ memoryOpt,
+ jvmOptsOpt,
+ forceOpt,
+ null,
+ readExtraArgs(extraArgsOpt));
+
+ if ("cloud".equals(exampleType)) {
+ runCloudExample(
+ new CloudExampleParams(noPromptOpt, promptInputsOpt, zkHostOpt, port, startParams));
+ } else {
+ runExample(new RunExampleParams(!userManagedOpt, zkHostOpt, port, startParams));
+ }
+ return 0;
}
protected boolean isPortAvailable(int port) {
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 89263c9da2ca..6926d66d7b11 100644
--- a/solr/core/src/java/org/apache/solr/cli/SnapshotCreateTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/SnapshotCreateTool.java
@@ -24,6 +24,15 @@
import org.apache.solr.client.solrj.response.CollectionAdminResponse;
/** Supports snapshot-create command in the bin/solr script. */
+@SuppressWarnings("UnnecessarilyFullyQualified")
+@picocli.CommandLine.Command(
+ name = "snapshot-create",
+ description = "Creates a named snapshot of a collection.",
+ footerHeading = "%nExamples:%n",
+ footer = {
+ " # Create a snapshot",
+ " bin/solr snapshot-create -c mycollection --snapshot-name snap1"
+ })
public class SnapshotCreateTool extends ToolBase {
private static final Option COLLECTION_NAME_OPTION =
@@ -48,6 +57,31 @@ public class SnapshotCreateTool extends ToolBase {
record SnapshotCreateParams(
String solrUrl, String credentials, String collectionName, String snapshotName) {}
+ // --- picocli fields ---
+
+ @picocli.CommandLine.ArgGroup(exclusive = true, multiplicity = "0..1")
+ private ConnectionOptions connectionOptions;
+
+ @picocli.CommandLine.Mixin private CredentialsOptions credentialsOptions;
+
+ @picocli.CommandLine.Option(
+ names = {"-c", "--name"},
+ required = true,
+ paramLabel = "NAME",
+ description = "Name of collection to be snapshot.")
+ private String nameOpt;
+
+ @picocli.CommandLine.Option(
+ names = "--snapshot-name",
+ required = true,
+ paramLabel = "NAME",
+ description = "Name of the snapshot to produce")
+ private String snapshotNameOpt;
+
+ public SnapshotCreateTool() {
+ this(new DefaultToolRuntime());
+ }
+
public SnapshotCreateTool(ToolRuntime runtime) {
super(runtime);
}
@@ -112,6 +146,13 @@ public void createSnapshot(SolrClient solrClient, String collectionName, String
@Override
public int callTool() throws Exception {
- throw new UnsupportedOperationException("This tool does not yet support PicoCli");
+ SnapshotCreateParams params =
+ new SnapshotCreateParams(
+ CLIUtils.resolveSolrUrl(connectionOptions, credentialsOptions.credentials),
+ credentialsOptions.credentials,
+ nameOpt,
+ snapshotNameOpt);
+ createSnapshot(params);
+ return 0;
}
}
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 7ff35e5e95a9..c660521a0994 100644
--- a/solr/core/src/java/org/apache/solr/cli/SnapshotDeleteTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/SnapshotDeleteTool.java
@@ -24,6 +24,15 @@
import org.apache.solr.client.solrj.response.CollectionAdminResponse;
/** Supports snapshot-delete command in the bin/solr script. */
+@SuppressWarnings("UnnecessarilyFullyQualified")
+@picocli.CommandLine.Command(
+ name = "snapshot-delete",
+ description = "Deletes a named snapshot of a collection.",
+ footerHeading = "%nExamples:%n",
+ footer = {
+ " # Delete a snapshot",
+ " bin/solr snapshot-delete -c mycollection --snapshot-name snap1"
+ })
public class SnapshotDeleteTool extends ToolBase {
private static final Option COLLECTION_NAME_OPTION =
@@ -48,6 +57,31 @@ public class SnapshotDeleteTool extends ToolBase {
record SnapshotDeleteParams(
String solrUrl, String credentials, String collectionName, String snapshotName) {}
+ // --- picocli fields ---
+
+ @picocli.CommandLine.ArgGroup(exclusive = true, multiplicity = "0..1")
+ private ConnectionOptions connectionOptions;
+
+ @picocli.CommandLine.Mixin private CredentialsOptions credentialsOptions;
+
+ @picocli.CommandLine.Option(
+ names = {"-c", "--name"},
+ required = true,
+ paramLabel = "NAME",
+ description = "Name of collection to manage.")
+ private String nameOpt;
+
+ @picocli.CommandLine.Option(
+ names = "--snapshot-name",
+ required = true,
+ paramLabel = "NAME",
+ description = "Name of the snapshot to delete")
+ private String snapshotNameOpt;
+
+ public SnapshotDeleteTool() {
+ this(new DefaultToolRuntime());
+ }
+
public SnapshotDeleteTool(ToolRuntime runtime) {
super(runtime);
}
@@ -112,6 +146,13 @@ public void deleteSnapshot(SolrClient solrClient, String collectionName, String
@Override
public int callTool() throws Exception {
- throw new UnsupportedOperationException("This tool does not yet support PicoCli");
+ SnapshotDeleteParams params =
+ new SnapshotDeleteParams(
+ CLIUtils.resolveSolrUrl(connectionOptions, credentialsOptions.credentials),
+ credentialsOptions.credentials,
+ nameOpt,
+ snapshotNameOpt);
+ deleteSnapshot(params);
+ return 0;
}
}
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 d945c23d57f6..aab58b6e8d30 100644
--- a/solr/core/src/java/org/apache/solr/cli/SnapshotDescribeTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/SnapshotDescribeTool.java
@@ -34,6 +34,15 @@
import org.apache.solr.core.snapshots.SolrSnapshotManager;
/** Supports snapshot-describe command in the bin/solr script. */
+@SuppressWarnings("UnnecessarilyFullyQualified")
+@picocli.CommandLine.Command(
+ name = "snapshot-describe",
+ description = "Describes a named snapshot of a collection.",
+ footerHeading = "%nExamples:%n",
+ footer = {
+ " # Describe a snapshot",
+ " bin/solr snapshot-describe -c mycollection --snapshot-name snap1"
+ })
public class SnapshotDescribeTool extends ToolBase {
private static final Option COLLECTION_NAME_OPTION =
@@ -58,6 +67,31 @@ public class SnapshotDescribeTool extends ToolBase {
record SnapshotDescribeParams(
String solrUrl, String credentials, String collectionName, String snapshotName) {}
+ // --- picocli fields ---
+
+ @picocli.CommandLine.ArgGroup(exclusive = true, multiplicity = "0..1")
+ private ConnectionOptions connectionOptions;
+
+ @picocli.CommandLine.Mixin private CredentialsOptions credentialsOptions;
+
+ @picocli.CommandLine.Option(
+ names = {"-c", "--name"},
+ required = true,
+ paramLabel = "NAME",
+ description = "Name of collection to be snapshot.")
+ private String nameOpt;
+
+ @picocli.CommandLine.Option(
+ names = "--snapshot-name",
+ required = true,
+ paramLabel = "NAME",
+ description = "Name of the snapshot to describe")
+ private String snapshotNameOpt;
+
+ public SnapshotDescribeTool() {
+ this(new DefaultToolRuntime());
+ }
+
public SnapshotDescribeTool(ToolRuntime runtime) {
super(runtime);
}
@@ -151,6 +185,13 @@ private Collection listCollectionSnapshots(
@Override
public int callTool() throws Exception {
- throw new UnsupportedOperationException("This tool does not yet support PicoCli");
+ SnapshotDescribeParams params =
+ new SnapshotDescribeParams(
+ CLIUtils.resolveSolrUrl(connectionOptions, credentialsOptions.credentials),
+ credentialsOptions.credentials,
+ nameOpt,
+ snapshotNameOpt);
+ describeSnapshot(params);
+ return 0;
}
}
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 8544122e261c..35aba0aafc1f 100644
--- a/solr/core/src/java/org/apache/solr/cli/SnapshotExportTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/SnapshotExportTool.java
@@ -28,6 +28,15 @@
import org.apache.solr.common.params.CollectionAdminParams;
/** Supports snapshot-export command in the bin/solr script. */
+@SuppressWarnings("UnnecessarilyFullyQualified")
+@picocli.CommandLine.Command(
+ name = "snapshot-export",
+ description = "Backs up a collection's current state to a local directory.",
+ footerHeading = "%nExamples:%n",
+ footer = {
+ " # Export a collection's current state as a backup",
+ " bin/solr snapshot-export -c mycollection --dest-dir /tmp/backups --backup-repo-name local"
+ })
public class SnapshotExportTool extends ToolBase {
private static final DateTimeFormatter BACKUP_NAME_TIMESTAMP =
@@ -90,6 +99,53 @@ record SnapshotExportParams(
String backupRepo,
String asyncReqId) {}
+ // --- picocli fields ---
+
+ @picocli.CommandLine.ArgGroup(exclusive = true, multiplicity = "0..1")
+ private ConnectionOptions connectionOptions;
+
+ @picocli.CommandLine.Mixin private CredentialsOptions credentialsOptions;
+
+ @picocli.CommandLine.Option(
+ names = {"-c", "--name"},
+ required = true,
+ paramLabel = "NAME",
+ description = "Name of the collection to be backed up.")
+ private String nameOpt;
+
+ // Accepted only so that passing it can be rejected with an explanation; see callTool().
+ @picocli.CommandLine.Option(
+ names = "--snapshot-name",
+ paramLabel = "NAME",
+ description = "No longer supported; passing it fails with an error.")
+ private String snapshotNameOpt;
+
+ @picocli.CommandLine.Option(
+ names = "--dest-dir",
+ required = true,
+ paramLabel = "DIR",
+ description =
+ "Path of a temporary directory on local filesystem during snapshot export command.")
+ private String destDirOpt;
+
+ @picocli.CommandLine.Option(
+ names = "--backup-repo-name",
+ paramLabel = "DIR",
+ description =
+ "Specifies name of the backup repository to be used during snapshot export preparation.")
+ private String backupRepoNameOpt;
+
+ @picocli.CommandLine.Option(
+ names = "--async-id",
+ paramLabel = "ID",
+ description =
+ "Specifies the async request identifier to be used during snapshot export preparation.")
+ private String asyncIdOpt;
+
+ public SnapshotExportTool() {
+ this(new DefaultToolRuntime());
+ }
+
public SnapshotExportTool(ToolRuntime runtime) {
super(runtime);
}
@@ -178,6 +234,21 @@ public void exportSnapshot(
@Override
public int callTool() throws Exception {
- throw new UnsupportedOperationException("This tool does not yet support PicoCli");
+ if (snapshotNameOpt != null) {
+ throw new IllegalArgumentException(
+ "--snapshot-name is no longer supported. Exporting a named snapshot required the "
+ + "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.");
+ }
+ SnapshotExportParams params =
+ new SnapshotExportParams(
+ CLIUtils.resolveSolrUrl(connectionOptions, credentialsOptions.credentials),
+ credentialsOptions.credentials,
+ nameOpt,
+ destDirOpt,
+ backupRepoNameOpt,
+ asyncIdOpt);
+ exportSnapshot(params);
+ return 0;
}
}
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 a4d7ea3b56ae..e7185f9ae11f 100644
--- a/solr/core/src/java/org/apache/solr/cli/SnapshotListTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/SnapshotListTool.java
@@ -26,6 +26,12 @@
import org.apache.solr.core.snapshots.SolrSnapshotManager;
/** Supports snapshot-list command in the bin/solr script. */
+@SuppressWarnings("UnnecessarilyFullyQualified")
+@picocli.CommandLine.Command(
+ name = "snapshot-list",
+ description = "Lists the snapshots for a collection.",
+ footerHeading = "%nExamples:%n",
+ footer = {" # List a collection's snapshots", " bin/solr snapshot-list -c mycollection"})
public class SnapshotListTool extends ToolBase {
private static final Option COLLECTION_NAME_OPTION =
@@ -40,6 +46,24 @@ public class SnapshotListTool extends ToolBase {
/** Parameters for the snapshot-list command, independent of the command line parser. */
record SnapshotListParams(String solrUrl, String credentials, String collectionName) {}
+ // --- picocli fields ---
+
+ @picocli.CommandLine.ArgGroup(exclusive = true, multiplicity = "0..1")
+ private ConnectionOptions connectionOptions;
+
+ @picocli.CommandLine.Mixin private CredentialsOptions credentialsOptions;
+
+ @picocli.CommandLine.Option(
+ names = {"-c", "--name"},
+ required = true,
+ paramLabel = "NAME",
+ description = "Name of collection to list snapshots for.")
+ private String nameOpt;
+
+ public SnapshotListTool() {
+ this(new DefaultToolRuntime());
+ }
+
public SnapshotListTool(ToolRuntime runtime) {
super(runtime);
}
@@ -99,6 +123,12 @@ public void listSnapshots(SolrClient solrClient, String collectionName) {
@Override
public int callTool() throws Exception {
- throw new UnsupportedOperationException("This tool does not yet support PicoCli");
+ SnapshotListParams params =
+ new SnapshotListParams(
+ CLIUtils.resolveSolrUrl(connectionOptions, credentialsOptions.credentials),
+ credentialsOptions.credentials,
+ nameOpt);
+ listSnapshots(params);
+ return 0;
}
}
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 a80d7f7883e8..00bf6e48b131 100755
--- a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java
+++ b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java
@@ -80,7 +80,22 @@
ZkTool.class,
AuthTool.class,
CreateTool.class,
- DeleteTool.class
+ DeleteTool.class,
+ PostTool.class,
+ StreamTool.class,
+ AssertTool.class,
+ HealthcheckTool.class,
+ ClusterTool.class,
+ ConfigTool.class,
+ ApiTool.class,
+ ExportTool.class,
+ PostLogsTool.class,
+ RunExampleTool.class,
+ SnapshotCreateTool.class,
+ SnapshotDeleteTool.class,
+ SnapshotDescribeTool.class,
+ SnapshotExportTool.class,
+ SnapshotListTool.class
})
public class SolrCLI implements CLIO, java.util.concurrent.Callable {
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 aa047f4b152c..fb1faed1fee8 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 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;
@@ -57,6 +58,19 @@
import org.apache.solr.handler.CatStream;
/** Supports stream command in the bin/solr script. */
+@SuppressWarnings("UnnecessarilyFullyQualified")
+@picocli.CommandLine.Command(
+ name = "stream",
+ description =
+ "Runs a streaming expression in Solr and prints the results, using the bin/solr CLI.",
+ 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 +90,67 @@ record StreamParams(
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;
+
+ @picocli.CommandLine.Option(
+ names = "--execution",
+ defaultValue = "remote",
+ paramLabel = "ENVIRONMENT",
+ description =
+ "Execution environment is either 'local' (i.e CLI process) or via a 'remote' Solr"
+ + " server. Default environment is 'remote'.")
+ private String 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);
}
@@ -534,7 +609,106 @@ static String listToString(List values, String internalDelim) {
@Override
public int callTool() throws Exception {
- throw new UnsupportedOperationException("This tool does not yet support PicoCli");
+ StreamParams params =
+ new StreamParams(
+ exprArgs,
+ executionOpt,
+ 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;
+ }
+
+ /**
+ * Resolves the base Solr URL used for remote execution and for probing whether Solr is running in
+ * cloud mode, honoring whichever of {@code --solr-url}, {@code --solr-connection} or {@code
+ * --zk-host} was given (mirrors {@link CLIUtils#normalizeSolrUrl(CommandLine)}).
+ */
+ private String resolveSolrUrl() throws Exception {
+ if (connectionOptions != null && connectionOptions.solrUrl != null) {
+ return CLIUtils.normalizeSolrUrl(connectionOptions.solrUrl);
+ }
+ String connectionString =
+ connectionOptions != null
+ ? (connectionOptions.solrConnection != null
+ ? connectionOptions.solrConnection
+ : connectionOptions.zkHost)
+ : null;
+ if (connectionString != null) {
+ return CLIUtils.solrUrlFromConnection(
+ CloudSolrClient.CloudSolrClientConnection.parse(connectionString),
+ 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}, resolving from picocli's connection options instead
+ * of a commons-cli {@code CommandLine}: an explicit {@code --solr-connection} or {@code
+ * --zk-host} wins outright, otherwise a running Solr instance is queried to see if it reports a
+ * ZooKeeper connection (SolrCloud mode), returning {@code null} if it does not.
+ */
+ private CloudSolrClient.CloudSolrClientConnection resolveSolrConnection() throws Exception {
+ if (connectionOptions != null && connectionOptions.solrConnection != null) {
+ return CloudSolrClient.CloudSolrClientConnection.parse(connectionOptions.solrConnection);
+ }
+ if (connectionOptions != null && connectionOptions.zkHost != null) {
+ var zkSolrConnection =
+ CloudSolrClient.CloudSolrClientConnection.parse(connectionOptions.zkHost);
+ if (!zkSolrConnection.isZookeeper()) {
+ throw new IOException(
+ String.format(
+ Locale.ROOT,
+ "Expected ZooKeeper connection string, but got: '%s'.",
+ connectionOptions.zkHost));
+ }
+ return zkSolrConnection;
+ }
+ try (SolrClient solrClient =
+ CLIUtils.getSolrClient(resolveSolrUrl(), credentialsOptions.credentials)) {
+ Map status = StatusTool.reportStatus(solrClient);
+ @SuppressWarnings("unchecked")
+ Map cloud = (Map) 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 {