From f9ce4cf1d7fa951eba0786d2085c0717d96b2834 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Wed, 3 Jun 2026 04:50:08 +0900 Subject: [PATCH 1/5] Fix search log flag query parsing (#2955) --- changelog.d/unreleased/2955.fixed.md | 17 +++++++++++++ src/CodeIndex/Cli/ProgramRunner.cs | 9 +++++++ src/CodeIndex/Cli/QueryCommandRunner.cs | 12 +++++++++ tests/CodeIndex.Tests/ProgramRunnerTests.cs | 27 +++++++++++++++++++++ 4 files changed, 65 insertions(+) create mode 100644 changelog.d/unreleased/2955.fixed.md diff --git a/changelog.d/unreleased/2955.fixed.md b/changelog.d/unreleased/2955.fixed.md new file mode 100644 index 0000000000..2edd325ca2 --- /dev/null +++ b/changelog.d/unreleased/2955.fixed.md @@ -0,0 +1,17 @@ +--- +category: fixed +issues: + - 2955 +affected: + - src/CodeIndex/Cli/ProgramRunner.cs + - src/CodeIndex/Cli/QueryCommandRunner.cs + - tests/CodeIndex.Tests/ProgramRunnerTests.cs +--- + +## English + +- **Search queries can now start with global log flag names (#2955)** — `search` no longer lets the global log flag parser consume a leading query such as `--log-max-size-mb`, so option-looking log flag text can be searched directly. + +## 日本語 + +- **`search` query の先頭に global log flag 名を指定できるようになりました (#2955)** — `--log-max-size-mb` のような query 先頭の文字列を global log flag parser が消費しなくなり、log flag に見える文字列をそのまま検索できます。 diff --git a/src/CodeIndex/Cli/ProgramRunner.cs b/src/CodeIndex/Cli/ProgramRunner.cs index 61e5e3e8c8..2afaaee9f3 100644 --- a/src/CodeIndex/Cli/ProgramRunner.cs +++ b/src/CodeIndex/Cli/ProgramRunner.cs @@ -581,6 +581,7 @@ internal static bool TryConsumeGlobalLogFlags(ref string[] args, out string erro error = string.Empty; var kept = new List(args.Length); var passthrough = false; + var searchCommandSeen = false; for (var i = 0; i < args.Length; i++) { var arg = args[i]; @@ -597,6 +598,12 @@ internal static bool TryConsumeGlobalLogFlags(ref string[] args, out string erro continue; } + if (searchCommandSeen) + { + kept.Add(arg); + continue; + } + if (TryConsumeValueFlag(args, ref i, arg, "--log-format", out var format)) { if (format is not ("text" or "json")) @@ -630,6 +637,8 @@ internal static bool TryConsumeGlobalLogFlags(ref string[] args, out string erro continue; } + if (arg == "search") + searchCommandSeen = true; kept.Add(arg); } diff --git a/src/CodeIndex/Cli/QueryCommandRunner.cs b/src/CodeIndex/Cli/QueryCommandRunner.cs index 007e776d37..1c9427c0b5 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.cs @@ -5469,6 +5469,18 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue) AddParseError("Error: --config is only supported by status."); } break; + case "--log-format": + case "--log-retain-count": + case "--log-max-size-mb": + if (allowNamedQuery && query == null) + { + query = currentArg; + } + else + { + AddParseError($"Error: unsupported option: {currentArg}. Use `--` before a query literal that starts with `-`."); + } + break; case "--path": if (TryReadStringOptionValue(args, ref i, "--path", inlineValue, allowSeparatedDashPrefixedLiteralValue: true, out var pathPattern, out var pathError)) { diff --git a/tests/CodeIndex.Tests/ProgramRunnerTests.cs b/tests/CodeIndex.Tests/ProgramRunnerTests.cs index 43c7428b77..55da37e71c 100644 --- a/tests/CodeIndex.Tests/ProgramRunnerTests.cs +++ b/tests/CodeIndex.Tests/ProgramRunnerTests.cs @@ -712,6 +712,33 @@ public void Run_ForcedGlobalToolLogging_JsonFormatWritesJsonLines() } } + [Fact] + public void Run_SearchQueryThatLooksLikeGlobalLogFlag_IsNotConsumed_Issue2955() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_issue2955_search_log_flag_query"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "USER_GUIDE.md", + "markdown", + "--log-max-size-mb appears here\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => ProgramRunner.Run( + ["search", "--log-max-size-mb", "--path", "USER_GUIDE.md", "--db", dbPath, "--count", "--exact-substring"], + appVersion: "1.10.0")); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal("1", stdout.Trim()); + Assert.Equal(string.Empty, stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + [Fact] public void Run_ForcedGlobalToolLogging_OnUnix_HardensExistingAndCurrentLogFiles() { From 0d966308d15abc9dd589431bde893913f297d7ed Mon Sep 17 00:00:00 2001 From: Widthdom Date: Wed, 3 Jun 2026 05:00:20 +0900 Subject: [PATCH 2/5] Preserve valid search log options (#2955) --- src/CodeIndex/Cli/ProgramRunner.cs | 16 +++++++++++- tests/CodeIndex.Tests/ProgramRunnerTests.cs | 29 +++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/CodeIndex/Cli/ProgramRunner.cs b/src/CodeIndex/Cli/ProgramRunner.cs index 487a510667..2a54f5edc9 100644 --- a/src/CodeIndex/Cli/ProgramRunner.cs +++ b/src/CodeIndex/Cli/ProgramRunner.cs @@ -598,7 +598,7 @@ internal static bool TryConsumeGlobalLogFlags(ref string[] args, out string erro continue; } - if (searchCommandSeen) + if (searchCommandSeen && IsSearchGlobalLogFlagLiteral(args, i, arg)) { kept.Add(arg); continue; @@ -646,6 +646,20 @@ internal static bool TryConsumeGlobalLogFlags(ref string[] args, out string erro return true; } + private static bool IsSearchGlobalLogFlagLiteral(string[] args, int index, string arg) + { + static bool NextTokenLooksLikeSearchOption(string[] args, int index) + => index + 1 >= args.Length || args[index + 1].StartsWith("-", StringComparison.Ordinal); + + if (arg is "--log-format" or "--log-retain-count" or "--log-max-size-mb") + return NextTokenLooksLikeSearchOption(args, index); + + return (arg.StartsWith("--log-format=", StringComparison.Ordinal) || + arg.StartsWith("--log-retain-count=", StringComparison.Ordinal) || + arg.StartsWith("--log-max-size-mb=", StringComparison.Ordinal)) && + NextTokenLooksLikeSearchOption(args, index); + } + private static bool TryConsumeValueFlag(string[] args, ref int index, string arg, string flag, out string value) { value = string.Empty; diff --git a/tests/CodeIndex.Tests/ProgramRunnerTests.cs b/tests/CodeIndex.Tests/ProgramRunnerTests.cs index 55da37e71c..423b575c75 100644 --- a/tests/CodeIndex.Tests/ProgramRunnerTests.cs +++ b/tests/CodeIndex.Tests/ProgramRunnerTests.cs @@ -739,6 +739,35 @@ public void Run_SearchQueryThatLooksLikeGlobalLogFlag_IsNotConsumed_Issue2955() } } + [Fact] + public void Run_SearchStillConsumesValidGlobalLogFlagBeforeQuery_Issue2955() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_issue2955_search_log_flag_option"); + using var env = EnvironmentVariableScope.Capture(GlobalToolLog.LogMaxSizeMbEnvironmentVariable); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "USER_GUIDE.md", + "markdown", + "needle appears here\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => ProgramRunner.Run( + ["search", "--log-max-size-mb", "1", "needle", "--path", "USER_GUIDE.md", "--db", dbPath, "--count"], + appVersion: "1.10.0")); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal("1", stdout.Trim()); + Assert.Equal(string.Empty, stderr); + Assert.Equal("1", Environment.GetEnvironmentVariable(GlobalToolLog.LogMaxSizeMbEnvironmentVariable)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + [Fact] public void Run_ForcedGlobalToolLogging_OnUnix_HardensExistingAndCurrentLogFiles() { From 5ee3b916254d865e4543d2ca46dacee991485624 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Wed, 3 Jun 2026 05:15:34 +0900 Subject: [PATCH 3/5] Accept inline log flag search queries (#2955) --- src/CodeIndex/Cli/QueryCommandRunner.cs | 4 ++- tests/CodeIndex.Tests/ProgramRunnerTests.cs | 29 +++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/CodeIndex/Cli/QueryCommandRunner.cs b/src/CodeIndex/Cli/QueryCommandRunner.cs index 1c9427c0b5..d6405b1f79 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.cs @@ -232,7 +232,9 @@ private sealed record StatusReadinessField( private const string OutputFormatJsonGraph = "json-graph"; private const string OutputFormatEdgeList = "edgelist"; private static readonly HashSet InlineValueOptions = - new(ValueTakingOptions.Concat(["--json"]), StringComparer.Ordinal); + new( + ValueTakingOptions.Concat(["--json", "--log-format", "--log-retain-count", "--log-max-size-mb"]), + StringComparer.Ordinal); private const string FindUsage = "Usage: cdidx find --path [--db ] [--json] [--format ] [--verbose] [--limit |--top ] [--lang ] [--exclude-path ] [--exclude-tests] [--before ] [--after ] [--snippet-lines ] [--focus-line ] [--focus-column ] [--max-line-width ] [--exact] [--regex] [--count]\n cdidx find --query --path [...]\n cdidx find [options] -- "; public static int RunBatch(string[] cmdArgs, JsonSerializerOptions jsonOptions) diff --git a/tests/CodeIndex.Tests/ProgramRunnerTests.cs b/tests/CodeIndex.Tests/ProgramRunnerTests.cs index 423b575c75..10c083a947 100644 --- a/tests/CodeIndex.Tests/ProgramRunnerTests.cs +++ b/tests/CodeIndex.Tests/ProgramRunnerTests.cs @@ -739,6 +739,35 @@ public void Run_SearchQueryThatLooksLikeGlobalLogFlag_IsNotConsumed_Issue2955() } } + [Theory] + [InlineData("--log-max-size-mb=50")] + [InlineData("--log-format=json")] + public void Run_SearchInlineQueryThatLooksLikeGlobalLogFlag_IsNotConsumed_Issue2955(string query) + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_issue2955_search_inline_log_flag_query"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "README.md", + "markdown", + $"{query} appears here\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => ProgramRunner.Run( + ["search", query, "--path", "README.md", "--db", dbPath, "--count", "--exact-substring"], + appVersion: "1.10.0")); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal("1", stdout.Trim()); + Assert.Equal(string.Empty, stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + [Fact] public void Run_SearchStillConsumesValidGlobalLogFlagBeforeQuery_Issue2955() { From f4644062b98a8b1f48bdf9aea3d243132ff78d38 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Wed, 3 Jun 2026 05:24:02 +0900 Subject: [PATCH 4/5] Preserve inline log options after search query (#2955) --- src/CodeIndex/Cli/ProgramRunner.cs | 108 +++++++++++++++++++- tests/CodeIndex.Tests/ProgramRunnerTests.cs | 29 ++++++ 2 files changed, 136 insertions(+), 1 deletion(-) diff --git a/src/CodeIndex/Cli/ProgramRunner.cs b/src/CodeIndex/Cli/ProgramRunner.cs index 2a54f5edc9..74e6e902ce 100644 --- a/src/CodeIndex/Cli/ProgramRunner.cs +++ b/src/CodeIndex/Cli/ProgramRunner.cs @@ -582,6 +582,9 @@ internal static bool TryConsumeGlobalLogFlags(ref string[] args, out string erro var kept = new List(args.Length); var passthrough = false; var searchCommandSeen = false; + var searchQuerySeen = false; + var pendingSearchOptionValue = false; + var pendingSearchOptionValueIsQuery = false; for (var i = 0; i < args.Length; i++) { var arg = args[i]; @@ -591,6 +594,16 @@ internal static bool TryConsumeGlobalLogFlags(ref string[] args, out string erro continue; } + if (searchCommandSeen && pendingSearchOptionValue) + { + if (pendingSearchOptionValueIsQuery) + searchQuerySeen = true; + pendingSearchOptionValue = false; + pendingSearchOptionValueIsQuery = false; + kept.Add(arg); + continue; + } + if (arg == "--") { passthrough = true; @@ -598,8 +611,9 @@ internal static bool TryConsumeGlobalLogFlags(ref string[] args, out string erro continue; } - if (searchCommandSeen && IsSearchGlobalLogFlagLiteral(args, i, arg)) + if (searchCommandSeen && !searchQuerySeen && IsSearchGlobalLogFlagLiteral(args, i, arg)) { + searchQuerySeen = true; kept.Add(arg); continue; } @@ -638,8 +652,14 @@ internal static bool TryConsumeGlobalLogFlags(ref string[] args, out string erro } if (arg == "search") + { searchCommandSeen = true; + kept.Add(arg); + continue; + } kept.Add(arg); + if (searchCommandSeen && !searchQuerySeen) + TrackSearchQueryState(args, i, arg, ref searchQuerySeen, ref pendingSearchOptionValue, ref pendingSearchOptionValueIsQuery); } args = kept.ToArray(); @@ -660,6 +680,92 @@ static bool NextTokenLooksLikeSearchOption(string[] args, int index) NextTokenLooksLikeSearchOption(args, index); } + private static void TrackSearchQueryState( + string[] args, + int index, + string arg, + ref bool searchQuerySeen, + ref bool pendingSearchOptionValue, + ref bool pendingSearchOptionValueIsQuery) + { + if (TryClassifySearchValueTakingOption(arg, out var hasInlineValue, out var valueIsQuery)) + { + if (hasInlineValue) + { + if (valueIsQuery) + searchQuerySeen = true; + } + else if (index + 1 < args.Length) + { + pendingSearchOptionValue = true; + pendingSearchOptionValueIsQuery = valueIsQuery; + } + return; + } + + if (!arg.StartsWith("-", StringComparison.Ordinal)) + searchQuerySeen = true; + } + + private static bool TryClassifySearchValueTakingOption(string arg, out bool hasInlineValue, out bool valueIsQuery) + { + hasInlineValue = false; + valueIsQuery = false; + + var separator = arg.IndexOf('='); + var optionName = separator > 0 ? arg[..separator] : arg; + if (!SearchValueTakingOptions.Contains(optionName)) + return false; + + hasInlineValue = separator > 0; + valueIsQuery = optionName == "--query"; + return true; + } + + private static readonly HashSet SearchValueTakingOptions = + [ + "--db", + "--data-dir", + "--limit", + "--top", + "--lang", + "--kind", + "--visibility", + "--exclude-visibility", + "--since", + "--start", + "--end", + "--before", + "--after", + "--name", + "--snippet-lines", + "--snippet-focus", + "--path", + "--require-before", + "--require-after", + "--reject-before", + "--reject-after", + "--guard-window", + "--project", + "--solution", + "--exclude-path", + "--max-hops", + "--depth", + "--query", + "--group-by", + "--focus-line", + "--focus-column", + "--focus-length", + "--max-line-width", + "--stale-after", + "--explain", + "--rank-by", + "--slow-query-ms", + "--format", + "--min-entrypoint-confidence", + "--sections", + ]; + private static bool TryConsumeValueFlag(string[] args, ref int index, string arg, string flag, out string value) { value = string.Empty; diff --git a/tests/CodeIndex.Tests/ProgramRunnerTests.cs b/tests/CodeIndex.Tests/ProgramRunnerTests.cs index 10c083a947..68fbb23d59 100644 --- a/tests/CodeIndex.Tests/ProgramRunnerTests.cs +++ b/tests/CodeIndex.Tests/ProgramRunnerTests.cs @@ -797,6 +797,35 @@ public void Run_SearchStillConsumesValidGlobalLogFlagBeforeQuery_Issue2955() } } + [Fact] + public void Run_SearchStillConsumesInlineGlobalLogFlagAfterQuery_Issue2955() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_issue2955_search_inline_log_flag_after_query"); + using var env = EnvironmentVariableScope.Capture(GlobalToolLog.LogMaxSizeMbEnvironmentVariable); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "USER_GUIDE.md", + "markdown", + "needle appears here\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => ProgramRunner.Run( + ["search", "needle", "--log-max-size-mb=1", "--path", "USER_GUIDE.md", "--db", dbPath, "--count"], + appVersion: "1.10.0")); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal("1", stdout.Trim()); + Assert.Equal(string.Empty, stderr); + Assert.Equal("1", Environment.GetEnvironmentVariable(GlobalToolLog.LogMaxSizeMbEnvironmentVariable)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + [Fact] public void Run_ForcedGlobalToolLogging_OnUnix_HardensExistingAndCurrentLogFiles() { From 1f2a67b04d7484bb6ce6548726c98b3cd5ca99b1 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Wed, 3 Jun 2026 05:34:24 +0900 Subject: [PATCH 5/5] Handle global value flags before log flag queries (#2955) --- src/CodeIndex/Cli/ProgramRunner.cs | 4 ++ tests/CodeIndex.Tests/ProgramRunnerTests.cs | 60 +++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/src/CodeIndex/Cli/ProgramRunner.cs b/src/CodeIndex/Cli/ProgramRunner.cs index 74e6e902ce..66e95599d0 100644 --- a/src/CodeIndex/Cli/ProgramRunner.cs +++ b/src/CodeIndex/Cli/ProgramRunner.cs @@ -725,7 +725,11 @@ private static bool TryClassifySearchValueTakingOption(string arg, out bool hasI private static readonly HashSet SearchValueTakingOptions = [ "--db", + "--color", "--data-dir", + "--metrics", + "--palette", + "--trace", "--limit", "--top", "--lang", diff --git a/tests/CodeIndex.Tests/ProgramRunnerTests.cs b/tests/CodeIndex.Tests/ProgramRunnerTests.cs index 68fbb23d59..ef45a14f87 100644 --- a/tests/CodeIndex.Tests/ProgramRunnerTests.cs +++ b/tests/CodeIndex.Tests/ProgramRunnerTests.cs @@ -826,6 +826,66 @@ public void Run_SearchStillConsumesInlineGlobalLogFlagAfterQuery_Issue2955() } } + [Theory] + [InlineData("--color", "never")] + [InlineData("--palette", "basic")] + [InlineData("--trace", "none")] + public void Run_SearchSeparatedGlobalValueFlagBeforeLogFlagQuery_IsNotMistakenForQuery_Issue2955(string optionName, string optionValue) + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_issue2955_search_global_value_before_log_flag_query"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "USER_GUIDE.md", + "markdown", + "--log-max-size-mb appears here\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => ProgramRunner.Run( + ["search", optionName, optionValue, "--log-max-size-mb", "--path", "USER_GUIDE.md", "--db", dbPath, "--count", "--exact-substring"], + appVersion: "1.10.0")); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal("1", stdout.Trim()); + Assert.Equal(string.Empty, stderr); + } + finally + { + ConsoleUi.SetColorMode(ColorMode.Auto); + ConsoleUi.SetColorPalette(null); + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void Run_SearchSeparatedMetricsFlagBeforeLogFlagQuery_IsNotMistakenForQuery_Issue2955() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_issue2955_search_metrics_before_log_flag_query"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + var metricsPath = Path.Combine(projectRoot, "metrics.jsonl"); + TestProjectHelper.InsertIndexedFile( + dbPath, + "USER_GUIDE.md", + "markdown", + "--log-max-size-mb appears here\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => ProgramRunner.Run( + ["search", "--metrics", metricsPath, "--log-max-size-mb", "--path", "USER_GUIDE.md", "--db", dbPath, "--count", "--exact-substring"], + appVersion: "1.10.0")); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal("1", stdout.Trim()); + Assert.Equal(string.Empty, stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + [Fact] public void Run_ForcedGlobalToolLogging_OnUnix_HardensExistingAndCurrentLogFiles() {