All findings · Lucene finding · LFV-339 · indexmergetool-double-dash-identity-compare

IndexMergeTool rejects '--' as an unrecognized option because it compares the argument with ==

IndexMergeTool -- -merged src1 src2 stops with "unrecognized option: '--'" and exit code 2 instead of merging into -merged: the parser's end-of-options check uses ==, which misses a command-line --, and it would also forget to skip the --. A small command-line defect since 9.0.0; ./-merged works around it.

Summary

Priority
Low, rank 1077IndexMergeTool rejects an undocumented '--' with a clear usage error and writes nothing: a minor CLI defect.
Impact
minor
Since
9.0.0 (LUCENE-9206, commit 93b83f635df, 2020-02-05)introduced 2020-02-05 · about 6 years
Affected
9.0.0-10.6.0 and main
Status
documented
Found by
non-formal: static analysis (Error Prone, PMD), then a JUnit

AI-assisted page: this finding was first signalled by code reading or testing and then checked with AI tools; the checks it went through, and what each found, are under Prior reports, history and checks. Not yet triaged by a human. Snapshot of 2026-11-06.

The finding in 30 seconds

Background: the tool, --, and == on Strings

  1. IndexMergeTool (module lucene-misc) merges indexes from the command line: IndexMergeTool [OPTION...] <mergedIndex> <index1> <index2> ..., with the options -merge-policy, -max-segments and -verbose.
  2. By a common command-line convention, -- ends the options: everything after it is a name, even a name that starts with -.
  3. In Java, a == b on two Strings asks whether they are the same object; a.equals(b) compares the text. A string literal in code is one shared (interned) object, while each command-line argument is a new String, so arg == "--" is false even when arg is --.
Diagram of the bug: IndexMergeTool's Options.parse reads the arguments --, -merged, src1, src2. The check args[index] == "--" compares object identity; a command-line argument is a new String, not the interned literal, so the check is false, -- reaches the option switch, and the tool prints unrecognized option: '--' and exits with code 2 without writing anything. With equals and index++ the -- is skipped and -merged becomes the target directory.
The arguments --, -merged, src1, src2 going through Options.parse: the identity check misses the command-line --, which falls to "unrecognized option", exit 2; with equals and index++ the -- is skipped and -merged becomes the target. Select the still to open it at full size. Generated with AI, checked against the code.

The finding, step by step

  1. You want to merge two indexes, src1 and src2, into a new directory named -merged. IndexMergeTool -merged src1 src2 stops with "unrecognized option: '-merged'": every leading argument that starts with - is read as an option.
  2. So you write IndexMergeTool -- -merged src1 src2, the usual way to say "options end here". The parser even has a branch for --; you expect "Merging...", "Done." and an index in -merged.
  3. Instead you get "unrecognized option: '--'", the usage text and exit code 2, and nothing is written. The branch tests args[index] == "--": == asks whether two Strings are the same object, and a command-line argument arrives as a new String, not the same object as the literal "--" in the code.
  4. The branch is also wrong when it does match, which only Java code passing the interned literal "--" can make happen: it does not skip the --, so -- becomes the target directory and -merged is read as the first source index.

Who is affected

Only people who run the misc IndexMergeTool command line (9.0.0 and later) and put -- before the paths: by habit, to name a target that starts with -, or from a wrapper script that adds --. They get a clean error with nothing written; ./-name works around it. No indexing or search API is affected.

Mechanism, terms and numbers518 words

IndexMergeTool, the misc module's command-line program that merges indexes, has an option parser whose branch for the end-of-options marker -- compares the argument with == instead of equals. The launcher passes each command-line argument as a new String, not the interned literal, so for a command-line -- the branch does not fire: -- falls through to the option switch and the tool stops with "unrecognized option: '--'" and exit code 2 before opening any directory. The branch is also missing index++, so if Java code passes the interned literal "--" to the public main(), -- itself becomes the merged-index path. Both defects date from LUCENE-9206 (commit 93b83f635df, 2020-02-05), first released in 9.0.0, and are still on main. Only this tool is affected (its command line and its main()), no indexing or search API; nothing is lost, and ./-name works around it. The fix is two lines: args[index].equals("--") and index++ before the break.

The bug

All lines are in lucene/misc/src/java/org/apache/lucene/misc/IndexMergeTool.java at 645d4fe14a20.

  1. Options.parse (:54-94) loops over the arguments and stops at the first one that does not start with - (:58-60); from there on the arguments are the target and the sources (:84-92).
  2. Next comes if (args[index] == "--") { break; } (:61-63), with no comment. == compares object identity, and the launcher gives main() a new String for each command-line argument (on OpenJDK 25 a JVM given -- prints args[0] == "--" false and args[0].equals("--") true), so for a command-line -- this is false.
  3. -- reaches switch (args[index]) (:64), which knows -merge-policy, -max-segments and -verbose; its default throws IllegalArgumentException("unrecognized option: '--'" + USAGE) (:77-79). main() prints the message and calls System.exit(2) (:99-104), before any directory is opened (:107-113).
  4. When the branch does fire (Java code passing the interned literal "--" to main() or to the package-private Options.parse), the break skips the index++ at :81. So mergedIndexPath = args[index] is "--" (:90), and the intended target becomes the first source (:91-92). The target is opened with OpenMode.CREATE (:51).
  5. Lucene's build had no Error Prone when this code landed; it came seven months later with LUCENE-9497 (17a3ba723ae, 2020-09-07), with its ReferenceEquality check off, and that check is still off ("ReferenceEquality:OFF", // noisy, build-tools/build-infra/src/main/java/org/apache/lucene/gradle/plugins/java/ErrorPronePlugin.java:738), so the build does not flag it.

On the command line

Stock jars of 645d4fe14a20, two one-document source indexes src1 and src2:

commandresult
IndexMergeTool -- -merged src1 src2unrecognized option: '--', usage text, exit 2, nothing written
IndexMergeTool -- merged src1 src2the same: unrecognized option: '--', exit 2
IndexMergeTool -merged src1 src2unrecognized option: '-merged', exit 2
IndexMergeTool merged src1 src2Merging..., Done., exit 0
IndexMergeTool ./-merged src1 src2Merging..., Done., exit 0 (the workaround)

From Java, IndexMergeTool.main(new String[] {"--", "target", "src1", "src2"}) merges into a new directory ./-- and reads target as a source; when target is not an index this ends in IndexNotFoundException (exit 1).

Exposure and likelihood182 words
  • Who is hit. Users of the command-line tool who type -- before the paths, and scripts that add it defensively. No query, document or index file reaches this parser.
  • Default vs opt-in. It is the tool's only parser; no setting is involved, and assertions make no difference.
  • Effect. A clean, immediate refusal with exit code 2 before any directory is opened; a probe found the working tree byte-for-byte unchanged. A target that starts with - cannot be passed with --; ./-name works.
  • Wrong output only from Java. The case where -- becomes the target needs Java code that passes the interned literal "--"; the merge then lands in ./-- (an index already there would be replaced, because the target is opened with OpenMode.CREATE), and the named target is only read: an existing index stays byte-identical, and a target that does not exist is left behind as an empty directory holding write.lock.
  • Reach. In the Lucene tree only the tool, its test, CHANGES.txt and MIGRATE.md mention it; a code search of Solr, Elasticsearch and OpenSearch (2026-10-26) found no use.

Minimal repro

One JUnit test, one call: Options.parse (package-private, the entry point the upstream TestIndexMergeTool uses) on --, target, s1, s2, with new String("--") standing in for a command-line argument. Deterministic, single-threaded, nothing random.

TestIndexMergeToolDoubleDash.java (JUnit, one test)29 lines
/*
 * 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.lucene.misc;

import org.apache.lucene.misc.IndexMergeTool.Options;
import org.apache.lucene.tests.util.LuceneTestCase;

public class TestIndexMergeToolDoubleDash extends LuceneTestCase {

  public void testDoubleDashEndsOptions() throws Exception {
    // expected: "--" ends the options (target "target"); actual: "unrecognized option: '--'"
    Options options = Options.parse(new String[] {new String("--"), "target", "s1", "s2"});
    assertEquals("target", options.mergedIndexPath);
  }
}
Command
cp TestIndexMergeToolDoubleDash.java $LUCENE/lucene/misc/src/test/org/apache/lucene/misc/
cd $LUCENE && ./gradlew :lucene:misc:test --tests org.apache.lucene.misc.TestIndexMergeToolDoubleDash -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false (same result)
Expected
-- ends the options and options.mergedIndexPath is target.
Observed
IllegalArgumentException: unrecognized option: '--' from Options.parse (IndexMergeTool.java:78).

All files of this repro: repro/LFV-339/

More files, environment, full output181 words

The one assertion covers both defects: on stock main the parse throws "unrecognized option: '--'"; with only the equals change it fails with expected:<[target]> but was:<[--]> (the missing index++); with the full fix it passes.

As the regression test: the same test, for example as a new method in TestIndexMergeTool.

Environment

lucenemain 645d4fe14a20 (11.0.0-SNAPSHOT); also fails on 10.6.0, 9.12.3 and 9.1.0
jdkOpenJDK 25.0.3 (build 25.0.3+9-LTS)
osLinux, aarch64
Command (environment)
cp TestIndexMergeToolDoubleDash.java $LUCENE/lucene/misc/src/test/org/apache/lucene/misc/
cd $LUCENE && ./gradlew :lucene:misc:test --tests org.apache.lucene.misc.TestIndexMergeToolDoubleDash -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false (same result)

Observed output

# gradle, :lucene:misc:test, seed DEADBEEF, main 645d4fe14a20 (trimmed; the same with -Ptests.asserts=false)
TestIndexMergeToolDoubleDash > testDoubleDashEndsOptions FAILED
    java.lang.IllegalArgumentException: unrecognized option: '--'
    Usage: IndexMergeTool [OPTION...] <mergedIndex> <index1> <index2> [index3] ...
    Merges source indexes 'index1' .. 'indexN' into 'mergedIndex'
    (rest of the usage text)
        at __randomizedtesting.SeedInfo.seed([DEADBEEF:205D5360B125C4B2]:0)
        at org.apache.lucene.misc.IndexMergeTool$Options.parse(IndexMergeTool.java:78)
        at org.apache.lucene.misc.TestIndexMergeToolDoubleDash.testDoubleDashEndsOptions(TestIndexMergeToolDoubleDash.java:26)

# the real command line, stock jars of 645d4fe14a20 (JVM start-up warnings filtered)
$ IndexMergeTool -- -merged src1 src2
unrecognized option: '--'
Usage: IndexMergeTool [OPTION...] <mergedIndex> <index1> <index2> [index3] ...
exit=2
$ IndexMergeTool ./-merged src1 src2
Merging...
Done.
exit=0

Why it looks unintended

  • The -- branch can only be meant to end the options, yet with == it does not fire for a command-line argument.
  • When it does fire, it leaves -- as the target path.
  • Nothing chose to reject --: the issue that added the parser never mentions it, and no test or comment covers it.
The full argument162 words
  • The branch. if (args[index] == "--") { break; } (IndexMergeTool.java:61-63) matches only -- and leaves the option loop; every verifier and the adjudication read it as the end-of-options rule.
  • History. LUCENE-9206 (#10246) added the whole parser in 93b83f635df; its discussion covers defaults, merge policy, -max-segments and forbidden-apis and never mentions --, and its patch already had the ==.
  • Tests. The upstream TestIndexMergeTool (7 tests) has no -- case. Its tests pass string literals, which are interned, so a test with the literal "--" would have hit the second defect, not the first.
  • The rest of the parser compares text. The switch on args[index] (:64) uses equals, as do other Lucene command-line parsers, for example "-level".equals(arg) in CheckIndex (lucene/core/src/java/org/apache/lucene/index/CheckIndex.java:4787).
  • Related precedent. #9095 (LUCENE-8047, open since 2017) lists String == comparisons in analysis code, which committers defended because only the same static final constants flow in. That reasoning does not hold for command-line arguments.
The devil's-advocate check247 words

The strongest case (the review and the works-as-designed review, two reading-only runs of the same reviewer from another model family, OpenAI GPT-5.6, with different lenses; the intended-behaviour checker): -- is documented nowhere in the repository (USAGE :36-45 lists only -merge-policy, -max-segments and -verbose; the class javadoc :29-32 names no option; lucene/CHANGES.txt:4866-4868 and lucene/MIGRATE.md:1280-1284 name only -max-segments; no upstream test uses --); the tool fails closed with a clear message; ./-name works; no other Lucene command-line tool supports --; and Lucene turned ReferenceEquality off on purpose.

Why it fails: this shows that -- support is undocumented, not that rejecting it was chosen. The author wrote a branch whose only purpose is to recognise --. Turning ReferenceEquality off was a project-wide call about a noisy check (LUCENE-9497, lucene-solr#1816), not a decision about this line. These points limit impact, not validity. The refute-lens review gave a procedural "not real" (it could not run code) while agreeing on both defects and rating the works-as-designed case weak; the works-as-designed review also rated it weak; the devil's advocate found the bug real with high confidence that it is not intended. A later cross-model fact check (2026-11-01) rated the works-as-designed case moderate, mainly because -- is documented nowhere, while agreeing that a branch written only for --, plus the missing index++, points to an unfinished implementation rather than a chosen rejection. Maintainers may well prefer to delete the branch, a legitimate answer that keeps today's behaviour.

Suggested fix

Recommended Compare with equals and skip the --

Compare with equals and skip the --: if (args[index].equals("--")) { index++; break; }. After -- every argument is a path, as the branch intends. Validated with the minimal test, TestIndexMergeTool and the whole misc test suite.

         if (!args[index].startsWith("-")) {
           break;
         }
-        if (args[index] == "--") {
+        if (args[index].equals("--")) {
+          index++;
           break;
         }
All options and trade-offs3 options · 407 words

Two options, both run through gradle in a scratch worktree of 645d4fe14a20 (removed afterwards); a third, half fix is not enough.

Compare with equals and skip the -- (validated by the checks) (recommended)

Makes the existing branch do what it was written for, the usual end-of-options rule. args[index] cannot be null here, because the line before already called startsWith on it.

         if (!args[index].startsWith("-")) {
           break;
         }
-        if (args[index] == "--") {
+        if (args[index].equals("--")) {
+          index++;
           break;
         }

Trade-offs

No API change; one new accepted input. Arguments after -- are read as paths, which adds no new filesystem reach, since ./-name already reaches the same directories. It does change the public main() for a Java caller that passes the interned "--": today the merge goes to ./--, with the fix to the next argument. One line in USAGE could mention --.

What the checks validated

Gradle in a scratch worktree of 645d4fe14a20 (2026-11-01): stock, the minimal test fails with assertions on and off; with this fix it passes with assertions on and off, TestIndexMergeTool passes 7 of 7 with it, and the whole :lucene:misc:test passes (24 classes, 317 tests, 0 failures, 9 skipped in other classes). Earlier, the same two source edits (the hunt's diff, which differs only in headers and paths) passed the hunt's 4 tests, the independent reproducer's 5 (including a real command-line run) and the minimal test as a standalone run.

Delete the dead branch (validated by the checks)

Remove IndexMergeTool.java:61-63, so -- is rejected on purpose, as it is today in practice.

         if (!args[index].startsWith("-")) {
           break;
         }
-        if (args[index] == "--") {
-          break;
-        }
         switch (args[index]) {

Trade-offs

Keeps today's command-line behaviour and removes the wrong-target path for Java callers; since -- was not documented or tested, this is a legitimate, contract-preserving choice maintainers may well prefer, but a target that starts with - still needs ./-name. The minimal test would then be the wrong test: a regression test would assert the rejection instead.

What the checks validated

Gradle in the same scratch worktree: TestIndexMergeTool passes 7 of 7; the minimal test still fails with "unrecognized option: '--'", as expected for this option.

Only replace == with equals (not enough)

Without index++, a command-line -- would become the target path.

Trade-offs

Worse than today's clean error: the merge would go to ./--. The minimal test fails with expected:<[target]> but was:<[--]>.

Downstream note

A code search of Solr, Elasticsearch and OpenSearch (2026-10-26) found no call to this tool; scripts that wrap it and put -- before the paths fail with exit code 2 until it is fixed.

How it was found

Non-formal: static analysis. In a sweep of Lucene's non-sandbox modules, Error Prone's ReferenceEquality check (which Lucene's build turns off as noisy) and two PMD rules all flagged IndexMergeTool.java:61. The finder read the warning at the code and confirmed it with a JUnit on the stock build.

The method in full100 words

Method: non-formal (static analysis, then execution). The hunt unit swept the non-sandbox modules with Error Prone 2.50, with ReferenceEquality enabled (it is off in Lucene's build), and PMD 7.28 (UseEqualsToCompareStrings, CompareObjectsWithEquals); every ReferenceEquality warning was read at the code. The unit was chosen by method, not with this bug in mind. A JUnit on the stock jars that calls Options.parse the way TestIndexMergeTool does then failed on main, 10.5.2 and 9.12.3; an independent reproducer added a run of the real command line in a child JVM, and the test was later cut to the one shown here.

Prior reports, history and checks

Prior reports

No prior report found in GitHub issues and PRs (including the migrated JIRA issues), open PRs, commits, the Lucene mailing lists, downstream repositories or the web (2026-10-26, re-checked 2026-11-01); not fixed upstream.

Searches4 searches

Closest items, none a report of this bug: #10246 (LUCENE-9206, introduced the parser; no mention of --); #9095 (LUCENE-8047, open since 2017, String == in analysis classes, filed more than two years before this code existed); lucene-solr#1816 and #10536 (LUCENE-9497, why ReferenceEquality is off); #13951 (the only open PR that touches the file; Options.parse untouched). Limits: keyword search only; GitHub search does not index code or diffs; private reports are not visible.

What was searched, when, and what turned up
WhereWhenHowFound
apache/lucene and apache/lucene-solr issues and PRs (incl. the migrated JIRA issues), downstream repositories, all of GitHub2026-10-26; re-checked 2026-11-01a scripted prior-report search (8 GitHub issue/PR, 2 commit, 2 downstream queries) and 27 GitHub GraphQL searches (IndexMergeTool, Options.parse, "end of options", "unrecognized option", ReferenceEquality, string comparison); re-check: IndexMergeTool issues and PRsno report; 5 IndexMergeTool issues, none about --; no PR
Open PRs2026-10-26changed files of all 411 open apache/lucene PRs; full diffs of the 3 largestonly #13951 touches the file, and leaves Options.parse alone
Mailing lists, JIRA archive, web2026-10-2660 scripted queries and manual searches over dev@, java-user@, issues@ and commits@; 12 JIRA-archive queries plus manual JIRA text search; 10 completed web searches (1 planned query stayed pending)no report
Commit history and upstream main2026-10-26; re-checked 2026-11-01git log -S 'args[index] == "--"', git log -S 'equals("--")'; the file's blob on upstream mainonly the introducing commit; unchanged on upstream main 2f424fd428da (2026-10-30)

History

Present since the option parser was added by LUCENE-9206 (93b83f635df, 2020-02-05; master only, first release 9.0.0). Every release from 9.0.0 to 10.6.0 and main is affected; 8.x had no option parser. Not a regression.

Versions tested6 rows

Introduced by LUCENE-9206 "Improve IndexMergeTool defaults and options" (Robert Muir; now issue #10246; a JIRA patch, no GitHub PR): commit 93b83f635df, 2020-02-05. It added the whole Options class with the == and the missing index++; git log -S and git blame find no other commit, and the four later commits that touch the file (196ec5f4a87, d1110394e9c, 2cbf261032d, 8ac26737913) change no parse logic. The author pushed it to master only ("For this command-line tool I will push to master only"); the CHANGES entry is in the 9.0.0 section, and the commit is an ancestor of the 9.0.0 tag but not of 8.5.0. By source, the line is present at all 33 release tags from 9.0.0 to 10.5.2 and at 10.6.0.

Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 affectedminimal test fails with assertions on and off (standalone and through Gradle); the real command line exits 2; passes with the fix
10.6.0 affectedminimal test fails with assertions on and off
10.5.2 affectedthe hunt's JUnit fails 3 of 4 (assertions on); the independent reproducer's 5-test JUnit fails 4 of 5 (assertions on and off); the command line exits 2
9.12.3, 9.1.0 affectedminimal test fails (assertions off); 9.12.3 also the hunt's JUnit, 3 of 4
10.0.0, 9.0.0 first affected: 9.0.0framework-free parse check: 4 of 4 bug checks fail, 3 controls pass; 9.0.0 command line exits 2
8.11.4, 8.4.1 no such APIno option parser at all (IndexMergeTool.Options does not exist); a leading -- is taken as the target path (-- merged src1 src2 creates ./--, then fails to open merged as a source, exit 1)

Independent checks

Confirmed with high confidence: an independent reproducer, a devil's advocate and an adjudication all reproduced it, and a reviewer from another model family (OpenAI GPT-5.6) rated the not-a-bug case weak. Everyone agrees the impact is small.

Checks and verdicts250 words

Re-run at registry time, in a credibility audit, on a later main and after minimisation: still reproduces.

Each independent check and its verdict
CheckVerdictWhat it found
finder (static analysis + JUnit) candidateError Prone ReferenceEquality and PMD at :61; a 4-test JUnit fails 3 of 4 on main (assertions on and off), 10.5.2 and 9.12.3; its control passes; the fix passes, and so do the 7 upstream tests.
cross-model review (OpenAI GPT-5.6, refute lens, reading only) not real, proceduralCould not run code; agreed on both defects but judged -- outside the documented usage; rated the works-as-designed case weak. A separate re-run failed 3 of 4.
independent reproducer realOwn 5 tests, including the real command line in a child JVM, fail 4 of 5 on main, 10.5.2 and 9.12.3; 9.0.0 reproduces; 8.11.4 has no parser; its fix passes.
cross-model adversary (OpenAI GPT-5.6, works-as-designed lens, reading only) real, mediumRated the intended case weak; separate re-runs fail on main, 10.5.2 and 9.12.3.
intended-behaviour devil's advocate real, highHigh confidence that rejecting -- is not intended, low impact; ran the real command line end to end, including the Java literal case.
adjudication confirmed, highRe-ran the reproducer's tests with assertions on and off (4 of 5 fail, control passes); impact class minor.
audit, refresh and minimisation confirmedCredibility audit confirmed it; still reproduces on main a9779ef3c193; minimised to the one-call test, which fails on 645d4fe14a20 and passes with the fix.

Diagram file

The diagram as a still image, generated with AI and checked against the code. It is under 10 MB, the limit for a GitHub attachment.

Sources and provenance

Finding record, Lucene and upstream sources, repro files and diagram notes1,087 words

Finding record

LFV-339, snapshot of 2026-11-06

Registry title
IndexMergeTool's '--' end-of-options branch compares Strings with ==, so a command-line '--' is rejected as an unrecognized option, and if the branch ever fires it leaves '--' as the target path
Area
misc / IndexMergeTool command-line tool (option parsing)
Priority
Low, rank 1077: IndexMergeTool rejects an undocumented '--' with a clear usage error and writes nothing: a minor CLI defect.
Impact class
minor
Impact note
Only users of the misc IndexMergeTool command-line tool (9.0.0 and later) who put '--' before the paths, for example to name a target directory that starts with '-', or a wrapper script that adds '--' defensively, are hit, and they get "unrecognized option: '--'", the usage text and exit code 2 before any directory is opened, so nothing is written or lost; '--' is not documented, './-name' works around it, no library API is affected, and the second defect (the matched '--' is not skipped, so it becomes the target path) needs Java code that passes the literal "--" to main().
Summary
You run the IndexMergeTool command line with '--' before the paths, for example 'IndexMergeTool -- -merged src1 src2', and expect the parser's dedicated '--' branch (IndexMergeTool.java:61) to end option parsing, as the usual command-line convention says, so that '-merged' is taken as the target directory. Instead the branch compares the argument with == against the literal "--", which is never true for a command-line argument, so '--' falls through to the option switch and the tool stops with "unrecognized option: '--'" and exit code 2 without merging anything. The branch is also wrong when it does fire (only when Java code passes the literal "--" to main()): it stops without skipping the '--', so '--' becomes the merged-index path and the intended target is read as the first source index.
Introduced
93b83f635dffc782dc70174e5ea377ceab6a8174 (LUCENE-9206 'Improve IndexMergeTool defaults and options', by rmuir; a JIRA patch, no GitHub PR; issue apache/lucene#10246) [read: in the full-history clone, git log -S 'args[index] == "--"' and git log -G '== "--"' over lucene/misc return only this commit, git blame -w -M -C on lines 61-62 agrees, and the attached LUCENE-9206.patch already has the comparison; the later commits 196ec5f4a87, d1110394e9c, 2cbf261032d and 8ac26737913 change no parse logic]. Original code, not a regression: '--' never ended the options in any release (2020-02-05 (commit to master; first released in 9.0.0, tagged 2021-12-06))
First affected version
9.0.0 [exec: framework-free parse check, 4 of 4 bug checks fail with the 3 controls passing, and the real CLI exits 2]. The change went to master only (the author wrote on LUCENE-9206 'For this command-line tool I will push to master only'; the CHANGES entry is under 9.0.0; 93b83f635df is an ancestor of releases/lucene/9.0.0 and not of releases/lucene-solr/8.5.0 or 8.8.1). 8.x has no option parser (8.4.1 and 8.11.4 executed: IndexMergeTool.Options does not exist)
Affected versions
9.0.0 through 10.6.0 and main ea57360912ed; the one-test minimal repro fails on the 10.6.0 release jars (SHA-1 checked against Maven Central) with -ea and -da [exec 2026-11-01]. [exec] main with -ea and -da (finder's JUnit 3 of 4 fail, the independent reproduction's JUnit 4 of 5 fail, parse check 4 of 4 fail, CLI exit 2), 10.5.2 and 9.12.3 with -ea and -da (JUnit), 10.0.0 (parse check), 9.0.0 (parse check and CLI exit 2; the JUnit repros do not compile against 9.0.0's test framework). [read] the line is present at all 33 release tags from 9.0.0 to 10.5.2, and IndexMergeTool.java has the same blob (0465f4e96b98) on upstream main e1c4184cb983, branch_10x, branch_10_6 and branch_9_12 (2026-10-26). Not affected: 8.x and older (no option parser). All runs on aarch64 with JDK 25.0.3
Status
documented (brief, checks and a per-finding doc exist)
Verification
Confirmed by AI-run checks: an independent repro, a cross-model review that could only read the code and voted “not real” while agreeing with the root cause (the adjudication ruled it real), an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
Found by
non-formal
Upstream and prior reports
none found (2026-10-26, read-only: a scripted prior-report search, 3 runs, 90 queries (8 GitHub issue/PR, 2 commit, 2 downstream-repo, 60 mailing-list over dev@, java-user@, issues@ and commits@, 12 JIRA-archive, 6 web of which 1 was not run); 27 GitHub GraphQL searches over apache/lucene, apache/lucene-solr, Elasticsearch, OpenSearch, Solr and all of GitHub; the changed files of all 411 open apache/lucene PRs; manual lists.apache.org and JIRA searches; git history; 10 further web searches; none the same bug). Closest items: #10246 (LUCENE-9206, introduced the parser; its discussion never mentions '--'), #9095 (LUCENE-8047, open since 2017: eight String ==/!= comparisons in analysis classes, which predates this code), lucene-solr#1816 (LUCENE-9497: Error Prone's ReferenceEquality turned off at integration, still 'ReferenceEquality:OFF, // noisy' at ErrorPronePlugin.java:738), #13951 (the only open PR that touches IndexMergeTool.java; Options.parse untouched). Not fixed upstream (line 61 unchanged on main e1c4184cb983, branch_10x, branch_10_6, branch_9_12)
Lucene commit tested
main 645d4fe14a20 (the minimal test with assertions on and off, standalone on 2026-10-29 and through gradle on 2026-11-01; the real command line); releases 10.6.0 (assertions on and off), 9.12.3 and 9.1.0 (assertions off) with the minimal test; the hunt's tests on main ea57360912ed, a9779ef3c193, 10.5.2 and 9.12.3; 10.0.0 and 9.0.0 with a framework-free check; 8.4.1 and 8.11.4 have no option parser. IndexMergeTool.java is the same blob at 645d4fe14a20, ea57360912ed, 10.6.0 and upstream main 2f424fd428da (2026-10-30).

Lucene and upstream sources

Finding record
  • lucene/misc/src/java/org/apache/lucene/misc/IndexMergeTool.java:29-32 (class javadoc), :36-45 (USAGE), :51 (OpenMode.CREATE), :54-95 (Options.parse; :58 startsWith("-"), :61 'if (args[index] == "--") { break; }', :77-79 unrecognized option, :90 mergedIndexPath = args[index]), :97-104 (main catches IllegalArgumentException and calls System.exit(2)) at ea57360912ed
  • lucene/CHANGES.txt:4833 (LUCENE-9206 entry under 9.0.0); lucene/MIGRATE.md:1280 (IndexMergeTool defaults changes)
  • build-tools/build-infra/src/main/java/org/apache/lucene/gradle/plugins/java/ErrorPronePlugin.java:738 ('ReferenceEquality:OFF', // noisy)
The finding in 30 seconds
  • lucene/misc/src/java/org/apache/lucene/misc/IndexMergeTool.java:36-45, :51, :54-94 (:58-63, :64-79, :81, :84-92), :97-113 (at 645d4fe14a20)
  • build-tools/build-infra/src/main/java/org/apache/lucene/gradle/plugins/java/ErrorPronePlugin.java:738 (at 645d4fe14a20)
Who is affected
  • lucene/misc/src/java/org/apache/lucene/misc/IndexMergeTool.java:51, :99-113 (at 645d4fe14a20)
Why it looks unintended
  • lucene/misc/src/java/org/apache/lucene/misc/IndexMergeTool.java:29-32, :36-45, :61-64 (at 645d4fe14a20)
  • lucene/CHANGES.txt:4866-4868; lucene/MIGRATE.md:1280-1284; lucene/misc/src/test/org/apache/lucene/misc/TestIndexMergeTool.java; lucene/core/src/java/org/apache/lucene/index/CheckIndex.java:4787 (at 645d4fe14a20)
Suggested fix
  • lucene/misc/src/java/org/apache/lucene/misc/IndexMergeTool.java:58-63 (at 645d4fe14a20)
History
  • lucene/CHANGES.txt:4866-4868 (9.0.0 section, at 645d4fe14a20)

Other statements trace to the hunt's working files (models, logs, reviews and briefs), which are not published.

Repro files

About the diagrams

Made in parallel with this doc from the code at 645d4fe14a20; its content follows the minimal repro and the command-line runs in this doc.

Built 2026-11-06 from the hunt's record of this finding and its doc file. Lucene main under test in this hunt: ea57360912ed. Every listed finding was re-run on Lucene main e1faeba08bde on 2026-11-02.