IndexMergeTool(modulelucene-misc) merges indexes from the command line:IndexMergeTool [OPTION...] <mergedIndex> <index1> <index2> ..., with the options-merge-policy,-max-segmentsand-verbose.- By a common command-line convention,
--ends the options: everything after it is a name, even a name that starts with-. - In Java,
a == bon 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, soarg == "--"is false even whenargis--.
--, -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.- You want to merge two indexes,
src1andsrc2, into a new directory named-merged.IndexMergeTool -merged src1 src2stops with "unrecognized option: '-merged'": every leading argument that starts with-is read as an option. - 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. - 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. - 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-mergedis read as the first source index.
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.
All lines are in lucene/misc/src/java/org/apache/lucene/misc/IndexMergeTool.java at 645d4fe14a20.
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).- Next comes
if (args[index] == "--") { break; }(:61-63), with no comment.==compares object identity, and the launcher givesmain()a new String for each command-line argument (on OpenJDK 25 a JVM given--printsargs[0] == "--"false andargs[0].equals("--")true), so for a command-line--this is false. --reachesswitch (args[index])(:64), which knows-merge-policy,-max-segmentsand-verbose; itsdefaultthrowsIllegalArgumentException("unrecognized option: '--'" + USAGE)(:77-79).main()prints the message and callsSystem.exit(2)(:99-104), before any directory is opened (:107-113).- When the branch does fire (Java code passing the interned literal
"--"tomain()or to the package-privateOptions.parse), thebreakskips theindex++at:81. SomergedIndexPath = args[index]is"--"(:90), and the intended target becomes the first source (:91-92). The target is opened withOpenMode.CREATE(:51). - Lucene's build had no Error Prone when this code landed; it came seven months later with LUCENE-9497 (
17a3ba723ae, 2020-09-07), with itsReferenceEqualitycheck 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.
Stock jars of 645d4fe14a20, two one-document source indexes src1 and src2:
| command | result |
|---|---|
IndexMergeTool -- -merged src1 src2 | unrecognized option: '--', usage text, exit 2, nothing written |
IndexMergeTool -- merged src1 src2 | the same: unrecognized option: '--', exit 2 |
IndexMergeTool -merged src1 src2 | unrecognized option: '-merged', exit 2 |
IndexMergeTool merged src1 src2 | Merging..., Done., exit 0 |
IndexMergeTool ./-merged src1 src2 | Merging..., 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--;./-nameworks. - 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 withOpenMode.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 holdingwrite.lock. - Reach. In the Lucene tree only the tool, its test,
CHANGES.txtandMIGRATE.mdmention it; a code search of Solr, Elasticsearch and OpenSearch (2026-10-26) found no use.
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.
/*
* 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);
}
}
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 andoptions.mergedIndexPathistarget.- Observed
IllegalArgumentException: unrecognized option: '--'fromOptions.parse(IndexMergeTool.java:78).
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.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT); also fails on 10.6.0, 9.12.3 and 9.1.0 |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux, aarch64 |
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)
# 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- 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-segmentsand 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
switchonargs[index](:64) usesequals, as do other Lucene command-line parsers, for example"-level".equals(arg)inCheckIndex(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 samestatic finalconstants 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.
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.
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.
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.
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:<[--]>.
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.
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.
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.
| Where | When | How | Found |
|---|---|---|---|
| apache/lucene and apache/lucene-solr issues and PRs (incl. the migrated JIRA issues), downstream repositories, all of GitHub | 2026-10-26; re-checked 2026-11-01 | a 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 PRs | no report; 5 IndexMergeTool issues, none about --; no PR |
| Open PRs | 2026-10-26 | changed files of all 411 open apache/lucene PRs; full diffs of the 3 largest | only #13951 touches the file, and leaves Options.parse alone |
| Mailing lists, JIRA archive, web | 2026-10-26 | 60 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 main | 2026-10-26; re-checked 2026-11-01 | git log -S 'args[index] == "--"', git log -S 'equals("--")'; the file's blob on upstream main | only the introducing commit; unchanged on upstream main 2f424fd428da (2026-10-30) |
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.
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | minimal test fails with assertions on and off (standalone and through Gradle); the real command line exits 2; passes with the fix |
| 10.6.0 | affected | minimal test fails with assertions on and off |
| 10.5.2 | affected | the 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 | affected | minimal 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.0 | framework-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 API | no 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) |
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.
| Check | Verdict | What it found |
|---|---|---|
| finder (static analysis + JUnit) | candidate | Error 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, procedural | Could 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 | real | Own 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, medium | Rated the intended case weak; separate re-runs fail on main, 10.5.2 and 9.12.3. |
| intended-behaviour devil's advocate | real, high | High confidence that rejecting -- is not intended, low impact; ran the real command line end to end, including the Java literal case. |
| adjudication | confirmed, high | Re-ran the reproducer's tests with assertions on and off (4 of 5 fail, control passes); impact class minor. |
| audit, refresh and minimisation | confirmed | Credibility audit confirmed it; still reproduces on main a9779ef3c193; minimised to the one-call test, which fails on 645d4fe14a20 and passes with the fix. |
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.
- PNG 429 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,087 words
- 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 mainea57360912ed,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.javais the same blob at645d4fe14a20,ea57360912ed, 10.6.0 and upstream main2f424fd428da(2026-10-30).
- 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 ea57360912edlucene/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)
- TestIndexMergeToolDoubleDash.java (JUnit, one test):
repro/LFV-339/src/org/apache/lucene/misc/TestIndexMergeToolDoubleDash.java(29 lines)
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.
assets/LFV-339/bug.png(429 KB)
![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.](../assets/LFV-339/bug.png)