Skip to content
Open
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
1 change: 1 addition & 0 deletions src/LogExpert.Configuration/LegacyPreferencesMigrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,5 @@ private static void MigrateToV1 (Settings settings)
}
#pragma warning restore CS0618
}

}
55 changes: 35 additions & 20 deletions src/LogExpert.Core/Classes/Log/RolloverFilenameBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ public class RolloverFilenameBuilder
#region Fields

private string _condContent;
private Group _condGroup;
private string _currentFileName;

private Group _dateGroup;
Expand All @@ -41,6 +40,7 @@ public class RolloverFilenameBuilder

private bool _hideZeroIndex;
private Group _indexGroup;
private Group _indexPartGroup;
private Regex _regex;

#endregion
Expand Down Expand Up @@ -89,7 +89,7 @@ public void SetFileName (string fileName)
Index = _indexGroup.Value.Length > 0 ? int.Parse(_indexGroup.Value) : 0;
}

_condGroup = match.Groups["cond"];
_indexPartGroup = match.Groups["indexPart"];
}
}

Expand All @@ -116,15 +116,15 @@ public string BuildFileName ()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A saved mask *$J(.).log previously behaved as *$J(.) and matched app.log.1; it now means app.1.log. That's what the spec wants, but existing user settings change

@Pr0metheus2 Pr0metheus2 Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the corrected syntax remain this way?

  • *$J(.) → app.log, app.log.1, app.log.2
  • *$J(.).log → app.log, app.1.log, app.2.log

If so, should I migrate existing saved *$J(.).log masks to *$J(.)?
(7)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added settings migration for corrected MultiFile mask behavior.

To preserve compatibility, settings version 2 migrates existing saved *$J(.).log preferences to *$J(.). New version-2 settings keep both patterns distinct:

  • *$J(.) for app.log.1
  • *$J(.).log for app.1.log

Added tests for migrating old settings and for ensuring new settings are not changed.

if (_indexGroup != null && _indexGroup.Success)
{
fileName = fileName.Remove(_indexGroup.Index, _indexGroup.Length);
fileName = fileName.Remove(_indexPartGroup.Index, _indexPartGroup.Length);

if (!_hideZeroIndex || Index > 0)
{
var format = "D" + _indexGroup.Length;
fileName = fileName.Insert(_indexGroup.Index, Index.ToString(format));
fileName = fileName.Insert(_indexPartGroup.Index, Index.ToString(format));
if (_hideZeroIndex && _condContent != null)
{
fileName = fileName.Insert(_indexGroup.Index, _condContent);
fileName = fileName.Insert(_indexPartGroup.Index, _condContent);
}
}
}
Expand All @@ -141,14 +141,14 @@ public string BuildFileName ()
private void ParseFormatString (string formatString)
{
var fmt = EscapeNonvarRegions(formatString);
var datePos = formatString.IndexOf("$D(", StringComparison.Ordinal);
var datePos = fmt.IndexOf("$D(", StringComparison.Ordinal);

if (datePos != -1)
{
var endPos = formatString.IndexOf(')', datePos);
var endPos = fmt.IndexOf(')', datePos);
if (endPos != -1)
{
_dateTimeFormat = formatString.Substring(datePos + 3, endPos - datePos - 3)
_dateTimeFormat = fmt.Substring(datePos + 3, endPos - datePos - 3)
.ToUpperInvariant()
.Replace('D', 'd')
.Replace('Y', 'y');
Expand Down Expand Up @@ -176,12 +176,17 @@ private void ParseFormatString (string formatString)
}
}

fmt = fmt.Replace("*", ".*", StringComparison.Ordinal);
fmt = fmt.Replace("*", ".*?", StringComparison.Ordinal);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.→.? and :194 \A…\z anchoring change every mask, not just ones with trailing literals. It holds because RolloverFilenameHandler.cs:39 passes a bare filename, not a path — but that invariant is undocumented and untested; a full path with any non-*-prefixed mask now fails where it previously matched.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept the end anchor needed to parse an index before fixed suffix, but removed the start anchor.
This retains compatibility with full paths and non-wildcard masks.
Added a full-path regression test.
(6)

_hideZeroIndex = fmt.Contains("$J", StringComparison.Ordinal);
fmt = fmt.Replace("$I", "(?'index'[\\d]+)", StringComparison.Ordinal);
fmt = fmt.Replace("$J", "(?'index'[\\d]*)", StringComparison.Ordinal);

_regex = new Regex(fmt);
const string indexPattern = "(?'indexPart'(?'index'[\\d]+))";
const string hiddenZeroIndexPattern = "(?'indexPart'(?'index'[\\d]*))";
var conditionalIndexPattern = _condContent != null
? $"(?'indexPart'(?:{Regex.Escape(_condContent)}(?'index'[\\d]+)|(?'index')))"
: hiddenZeroIndexPattern;
fmt = fmt.Replace("$I", indexPattern, StringComparison.Ordinal);
fmt = fmt.Replace("$J", conditionalIndexPattern, StringComparison.Ordinal);

_regex = new Regex(fmt + @"\z");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$I digit class went from one-or-more to zero-or-more. A plain app.log now matches *$I.log as index 0, so $I behaves like $J when the opened file has no digits.

*$J(.) no longer behaves exactly as before. The lazy wildcard plus the end anchor mean opening engine.log.1 now yields index 1. Previously it yielded index 0 and generated engine.log.1.1. This is a fix, but it is an unstated behaviour change.

The FlushEscaped method named in the PR description does not exist. The helpers are AppendEscapedSegment and AppendSegment.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(2) Restored $I behavior:

  • $I again requires one or more digits ([\d]+).
  • app.log no longer matches *$I.log.
  • $J retains its zero-index handling and the rotated-file parsing fix.
  • The corrected behavior for opening engine.log.1 is kept; describe that explicitly in the PR rather than treating it as invisible behavior.

Note: I cannot find FlushEscaped in the current PR description.

}

private string EscapeNonvarRegions (string formatString)
Expand All @@ -199,17 +204,15 @@ private string EscapeNonvarRegions (string formatString)
case 0: // looking for $
if (fmt[i] == '$')
{
_ = result.Append(Regex.Escape(segment.ToString()));
segment = new StringBuilder();
AppendEscapedSegment(result, segment);
state = 1;
}

_ = segment.Append(fmt[i]);
break;
case 1: // the char behind $
_ = segment.Append(fmt[i]);
_ = result.Append(segment);
segment = new StringBuilder();
AppendSegment(result, segment);
state = 2;
break;
case 2: // checking if ( or other char
Expand All @@ -229,18 +232,30 @@ private string EscapeNonvarRegions (string formatString)
_ = segment.Append(fmt[i]);
if (fmt[i] == ')')
{
_ = result.Append(segment);
segment = new StringBuilder();
AppendSegment(result, segment);
state = 0;
}

break;
}
}

AppendEscapedSegment(result, segment);
fmt = result.ToString().Replace('\xFFFD', '*');
return fmt;
}

private static void AppendEscapedSegment (StringBuilder result, StringBuilder segment)
{
_ = result.Append(Regex.Escape(segment.ToString()));
_ = segment.Clear();
}

private static void AppendSegment (StringBuilder result, StringBuilder segment)
{
_ = result.Append(segment);
_ = segment.Clear();
}

#endregion
}
}
12 changes: 8 additions & 4 deletions src/LogExpert.Resources/Resources.de.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1680,12 +1680,16 @@ Ein ausgewähltes Tool erscheint in der Iconbar. Alle anderen verfügbaren Tools
<value>Muster syntax:

* = alle Zeichen (wildcard)
$D(&amp;lt;date&amp;gt;) = Datumsmuster
$D(&lt;date&gt;) = Datumsmuster
$I = Dateiindexnummer
$J = Dateiindexnummer, versteckt wenn 09
$J(&amp;lt;prefix&amp;gt;) = Wie $J, jedoch wird ein &amp;lt;prefix&amp;gt; hinzugefügt when es nicht 0 ist
$J = Dateiindexnummer, versteckt wenn null
$J(&lt;prefix&gt;) = Wie $J, jedoch wird ein &lt;prefix&gt; hinzugefügt wenn es nicht 0 ist

&amp;lt;date&amp;gt;:
Beispiele:
*$J(.) → app.log, app.log.1, app.log.2
*$J(.).log → app.log, app.1.log, app.2.log

&lt;date&gt;:
DD = Tag
MM = Monat
YY[YY] = Jahr
Expand Down
10 changes: 7 additions & 3 deletions src/LogExpert.Resources/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1743,12 +1743,16 @@ Checked tools will appear in the icon bar. All other tools are available in the
<value>Pattern syntax:

* = any characters (wildcard)
$D(&amp;lt;date&amp;gt;) = Date pattern
$D(&lt;date&gt;) = Date pattern
$I = File index number
$J = File index number, hidden when zero
$J(&amp;lt;prefix&amp;gt;) = Like $J, but adding &amp;lt;prefix&amp;gt; when non-zero
$J(&lt;prefix&gt;) = Like $J, but adding &lt;prefix&gt; when non-zero

&amp;lt;date&amp;gt;:
Examples:
*$J(.) → app.log, app.log.1, app.log.2
*$J(.).log → app.log, app.1.log, app.2.log

&lt;date&gt;:
DD = day
MM = month
YY[YY] = year
Expand Down
4 changes: 4 additions & 0 deletions src/LogExpert.Resources/Resources.zh-CN.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1554,6 +1554,10 @@ $I = 文件索引编号
$J = 文件索引编号,为零时隐藏
$J(&lt;前缀&gt;) = 类似 $J,但在非零时添加 &lt;前缀&gt;

示例:
*$J(.) → app.log, app.log.1, app.log.2
*$J(.).log → app.log, app.1.log, app.2.log

&lt;日期&gt;:
DD = 日
MM = 月
Expand Down
6 changes: 6 additions & 0 deletions src/LogExpert.Tests/LogExpert.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@
<Content Include="TestData\app.log.1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="TestData\app.1.log">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="TestData\app.2.log">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<None Update="TestData\organizations-1000.csv">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
Expand Down
29 changes: 28 additions & 1 deletion src/LogExpert.Tests/RollingNameTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ public void TestFilename1(string expectedResult, string formatString)
[TestCase("engine.log", "engine1.log","engine$J.log")]
[TestCase("engine1.log", "engine2.log","engine$J.log")]
[TestCase("engine.log", "engine.log.1","*$J(.)")]
[TestCase("engine.log", "engine.1.log", "*$J(.).log")]
[TestCase("engine.log.1", "engine.log.2", "*$J(.)")]
[TestCase("engine.1.log", "engine.2.log", "*$J(.).log")]
[TestCase("engine1.log", "engine2.log", "*$I.log")]
[TestCase("engine_2010-06-12.1.log", "engine_2010-06-12.2.log", "*$D(yyyy-MM-dd)$J(.).log")]
[TestCase("engine_2010-06-12.log", "engine_2010-06-12.log.1", "*$D(yyyy-MM-dd).log$J(.)")]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$I untested. For "$J, $I" support, the fix itself generalizes, but no test covers $I or $D followed by a trailing literal (e.g. *$D(yyyy-MM-dd)$J(.).log).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added cases which cover:

  • $I with a trailing literal .log, starting from a rotated file.
  • $D plus $J(.) followed by a trailing .log, also starting from a rotated file.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ParseFormatString computes datePos from the original string but edits the escaped one — a literal with regex metachars before $D (e.g. app.$D(yyyy-MM-dd)) corrupts the regex. Not introduced here, but the new trailing-literal support makes such patterns likelier.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed by locating and parsing $D(...) in the escaped format string. Offsets remain correct when literal regex metacharacters appear before the date placeholder.
Added a regression test for app.$D(yyyy-MM-dd).log that verifies date parsing and rebuilding.

public void TestFilenameAnd1(string fileName, string expectedResult, string formatString)
{
Expand All @@ -45,6 +50,7 @@ public void TestFilenameAnd1(string fileName, string expectedResult, string form
[Test]
[TestCase("engine.log", "engine.log.2","*$J(.)")]
[TestCase("engine.log", "engine.log.2","*.log$J(.)")]
[TestCase("engine.log", "engine.2.log", "*$J(.).log")]
public void TestFilenameAnd2(string fileName, string expectedResult, string formatString)
{
RolloverFilenameBuilder fnb = new(formatString);
Expand All @@ -54,6 +60,27 @@ public void TestFilenameAnd2(string fileName, string expectedResult, string form
Assert.That(name, Is.EqualTo(expectedResult));
}

[Test]
public void TestFilenameDateAfterRegexMetacharacter ()
{
RolloverFilenameBuilder fnb = new("app.$D(yyyy-MM-dd).log");
fnb.SetFileName("app.2010-06-12.log");

fnb.IncrementDate();

Assert.That(fnb.BuildFileName(), Is.EqualTo("app.2010-06-13.log"));
}

[Test]
public void BuildFileName_FullPathWithNonWildcardMask_IncrementsIndex ()
{
RolloverFilenameBuilder fnb = new("engine$J.log");
fnb.SetFileName(@"C:\logs\engine1.log");

fnb.Index += 1;

Assert.That(fnb.BuildFileName(), Is.EqualTo(@"C:\logs\engine2.log"));
}

[Test]
[TestCase("engine1.log", "engine.log","engine$J.log")]
Expand All @@ -65,4 +92,4 @@ public void TestFilenameMinus1(string fileName, string expectedResult, string fo
var name = fnb.BuildFileName();
Assert.That(name, Is.EqualTo("engine.log"));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,24 @@ public void SingleFileCtor_MultiFileTrue_ExpandsRollover ()
});
}

[Test]
public void SingleFileCtor_MultiFileTrue_LoadsIndexBeforeExtension ()
{
var options = new MultiFileOptions { FormatPattern = "*$J(.).log" };

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The $I case is only tested from the active file, despite the PR description claiming a rotated-file $I test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(3) Updated the $I test to start from a rotated filename:
engine1.log → engine2.log
with *$I.log.

File.Copy(Path.Combine(_testDataDirectory, "app.1.log"), Path.Combine(_testDirectory, "app.1.log"));
File.Copy(Path.Combine(_testDataDirectory, "app.2.log"), Path.Combine(_testDirectory, "app.2.log"));
using var reader = CreateSingleFileReader(multiFile: true, options);

reader.ReadFiles();

Assert.Multiple(() =>
{
Assert.That(reader.IsMultiFile, Is.True);
Assert.That(reader.GetLogFileInfoList().Select(file => Path.GetFileName(file.FullName)),
Is.EqualTo(new[] { "app.2.log", "app.1.log", "app.log" }));
});
}

[Test]
public void MultiFileCtor_AlwaysMultiFile ()
{
Expand All @@ -91,15 +109,15 @@ public void MultiFileCtor_AlwaysMultiFile ()
});
}

private LogfileReader CreateSingleFileReader (bool multiFile)
private LogfileReader CreateSingleFileReader (bool multiFile, MultiFileOptions? options = null)
{
return new LogfileReader(
_logFile,
new EncodingOptions { Encoding = Encoding.UTF8 },

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

opens the active file only. Opening a rotated file directly never picks up lower siblings because the name-list walk only goes upward. That is pre-existing, but the spec is only met when the active file is opened.

No round-trip test for *$J(.).log.

@Pr0metheus2 Pr0metheus2 Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is that this is intended behavior: when a rotated file such as app.1.log is opened, MultiFile loads that file and higher indexes only. To load the complete chain including lower indexes, the user should open the active file (app.log). If user want to open log file from specified date only and further, i.e. app.8.log, this is a useful behaviour and in this case app will load app.8.log -> app.9.log etc.

Please confirm whether this is correct. If MultiFile should also discover lower-index siblings when a rotated file is opened directly, I can extend the handler and add coverage for that behavior.

multiFile,
bufferCount: 40,
linesPerBuffer: 50,
new MultiFileOptions(),
options ?? new MultiFileOptions(),
ReaderType.System,
PluginRegistry.PluginRegistry.Instance,
maximumLineLength: 500,
Expand Down
1 change: 1 addition & 0 deletions src/LogExpert.Tests/TestData/app.1.log
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
app.1.log
1 change: 1 addition & 0 deletions src/LogExpert.Tests/TestData/app.2.log
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
app.2.log

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 6 additions & 6 deletions src/LogExpert.UI/Dialogs/MultiFileMaskDialog.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading