Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions changelog.d/unreleased/2955.fixed.md
Original file line number Diff line number Diff line change
@@ -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 に見える文字列をそのまま検索できます。
133 changes: 133 additions & 0 deletions src/CodeIndex/Cli/ProgramRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,10 @@ internal static bool TryConsumeGlobalLogFlags(ref string[] args, out string erro
error = string.Empty;
var kept = new List<string>(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];
Expand All @@ -590,13 +594,30 @@ 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;
kept.Add(arg);
continue;
}

if (searchCommandSeen && !searchQuerySeen && IsSearchGlobalLogFlagLiteral(args, i, arg))
{
searchQuerySeen = true;
kept.Add(arg);
continue;
}

if (TryConsumeValueFlag(args, ref i, arg, "--log-format", out var format))
{
if (format is not ("text" or "json"))
Expand Down Expand Up @@ -630,13 +651,125 @@ internal static bool TryConsumeGlobalLogFlags(ref string[] args, out string erro
continue;
}

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();
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 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<string> SearchValueTakingOptions =
[
"--db",
"--color",
"--data-dir",
"--metrics",
"--palette",
"--trace",
"--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;
Expand Down
16 changes: 15 additions & 1 deletion src/CodeIndex/Cli/QueryCommandRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,9 @@ private sealed record StatusReadinessField(
private const string OutputFormatJsonGraph = "json-graph";
private const string OutputFormatEdgeList = "edgelist";
private static readonly HashSet<string> 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 <query> --path <glob> [--db <path>] [--json] [--format <text|json|count|compact|csv|tsv|lsp|qf|sarif>] [--verbose] [--limit <n>|--top <n>] [--lang <lang>] [--exclude-path <glob>] [--exclude-tests] [--before <n>] [--after <n>] [--snippet-lines <n>] [--focus-line <line>] [--focus-column <n>] [--max-line-width <n>] [--exact] [--regex] [--count]\n cdidx find --query <query> --path <glob> [...]\n cdidx find [options] -- <query>";

public static int RunBatch(string[] cmdArgs, JsonSerializerOptions jsonOptions)
Expand Down Expand Up @@ -5469,6 +5471,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))
{
Expand Down
174 changes: 174 additions & 0 deletions tests/CodeIndex.Tests/ProgramRunnerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,180 @@ 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);
}
}

[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()
{
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_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);
}
}

[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()
{
Expand Down
Loading