Skip to content

Commit fab8aae

Browse files
committed
feat: support single-file path targets
1 parent 6128f57 commit fab8aae

10 files changed

Lines changed: 144 additions & 22 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
# Changelog
22

3-
## [v1.2] - 2026-06-16
3+
## [v1.3] - 2026-06-16
4+
5+
### Added
6+
7+
- Added `path=<file>` support so EncodingFixTool can process exactly one file while using the file's parent directory as the scan root.
8+
- Added regression coverage for single-file CLI processing to ensure sibling files are not touched.
9+
10+
## [v1.2] - 2026-06-16
411

512
### Added
613

README.md

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# EncodingFixTool
22

33
A fast, parallel, command-line fixer for Delphi source file encodings.
4-
It scans `.pas`, `.dpr`, …; detects UTF-8/ASCII vs. legacy single-byte encodings; repairs mixed lines (Windows-1250/1252/ANSI), and writes clean UTF-8 with an optional BOM. Line endings are preserved by default, or normalized to Windows CRLF when requested.
4+
It scans a directory tree or one explicit file; detects UTF-8/ASCII vs. legacy single-byte encodings; repairs mixed lines (Windows-1250/1252/ANSI), and writes clean UTF-8 with an optional BOM. Line endings are preserved by default, or normalized to Windows CRLF when requested.
55

66
---
77

@@ -31,6 +31,9 @@ EncodingFixTool path=C:\Projs\MyApp v bkp-dir=C:\backup\myapp
3131
:: Only scan .pas files in ./src (not recursive), remove BOM if present
3232
EncodingFixTool path=.\src recursive=n ext=pas utf8-bom=n
3333
34+
:: Process exactly one file
35+
EncodingFixTool path=.\src\GeneratedUnit.pas preset=delphi-ai
36+
3437
:: Normalize generated Delphi sources to Windows CRLF while fixing encodings
3538
EncodingFixTool path=.\src ext=pas,dpr eol=crlf
3639
@@ -50,7 +53,7 @@ EncodingFixTool ext="*.pas,*.dpr, .dfm"
5053
| `dry` ||| off | Dry run: analyze and report what **would** change; no writes. |
5154
| `s`/`silent` ||| off | No console output. (Overrides `verbose`.) |
5255
| `v`/`verbose` ||| off | More output: “OK” lines etc. (Ignored if `silent`.) |
53-
| `path` || dir | current dir | Directory to scan. |
56+
| `path` || dir/file | current dir | Directory to scan, or one file to process. |
5457
| `recursive` || `y`/`n`/`yes`/`no`/`1`/`0` | `y` | Recurse into subfolders. |
5558
| `ext` || CSV list | `pas,dpr` | File extensions to include. Smart parsing: accepts `pas`, `.pas`, `*.pas`. Quoted lists OK. |
5659
| `preset` || `delphi-ai` || Applies Delphi agent cleanup defaults: Delphi extensions, UTF-8 BOM, recursive scan, CRLF. |
@@ -75,7 +78,7 @@ C:\bkp\src\foo\bar\Main.pas
7578
## What it does (algorithm)
7679

7780
1. **Gather files**
78-
Walk the `path` (recursively by default), matching the configured extensions.
81+
If `path` names a file, process only that file. Otherwise walk the directory `path` (recursively by default), matching the configured extensions.
7982

8083
2. **Process in parallel**
8184
Uses `TParallel.For` to utilize multiple cores. Console output is synchronized, and the “files changed” and failure counters are atomic.
@@ -151,7 +154,7 @@ Done in 00:08.972. Files changed: 2. Failures: 0
151154
Binary Delphi forms are detected from raw bytes and skipped unchanged. Text DFM files remain eligible for the normal encoding and optional line-ending repair path.
152155

153156
* **Relative reporting**
154-
Paths in logs are shown **relative to** the scanned `path`, for readability.
157+
Paths in logs are shown **relative to** the scanned directory, for readability. In single-file mode the scan root is the file's parent directory.
155158

156159
## AI/agent usage
157160

@@ -173,6 +176,12 @@ Use dry-run mode first when the dirty worktree is not fully understood:
173176
EncodingFixTool path=. preset=delphi-ai scope=git-changed format=json dry
174177
```
175178

179+
For the narrowest safe repair, point `path` at one file:
180+
181+
```powershell
182+
EncodingFixTool path=.\src\GeneratedUnit.pas preset=delphi-ai format=json
183+
```
184+
176185
Do not run the non-dry command over broad third-party or vendored code unless that is the intended scope. Inspect the JSON summary and `git diff --stat` before committing.
177186

178187
## Agent skill
@@ -265,4 +274,4 @@ EncodingFixTool pairs well with [MaxLogic Delphi Companion](https://github.com/M
265274

266275
* `0` – completed without parse errors or per-file failures.
267276
* `1` – invalid parameter or one or more files failed to process.
268-
* `2` – the `path` argument didn’t exist.
277+
* `2` – the `path` argument didn’t exist as either a directory or a file.

TASKS.md

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# Tasks
22

3-
Next task ID: T-007
3+
Next task ID: T-008
44

55
## Summary
66
Open tasks: 0 (In Progress: 0, Next Today: 0, Next This Week: 0, Next Later: 0, Blocked: 0)
7-
Done tasks: 6
7+
Done tasks: 7
88

99
## In Progress
1010

@@ -18,6 +18,35 @@ Done tasks: 6
1818

1919
## Done
2020

21+
### T-007 [CLI] Support single-file path targets
22+
Completed: 2026-06-16
23+
Outcome:
24+
- `path=<file>` is accepted as a first-class narrow target.
25+
- Single-file mode uses the file's parent directory as the scan root and processes only that file when its extension matches the active options or preset.
26+
- README, agent skill, reference docs, and changelog document the single-file workflow.
27+
Proof:
28+
- PASS: `.\bin\EncodingFixTool.Tests.exe --include:SingleFile`
29+
Result: exit=0, 1 passed, 0 failed
30+
- PASS: `.\bin\EncodingFixTool.Tests.exe`
31+
Result: exit=0, 31 passed, 0 failed
32+
- PASS: `powershell -NoProfile -ExecutionPolicy Bypass -File tests\Invoke-EncodingFixTool.Tests.ps1`
33+
Result: exit=0, output contains `EncodingFixTool CLI tests passed.`
34+
- PASS: `Invoke-ScriptAnalyzer -Path .\tests\Invoke-EncodingFixTool.Tests.ps1 -Severity Warning,Error`
35+
Result: exit=0, no findings
36+
- PASS: `cmd /s /c '"C:\Program Files (x86)\Embarcadero\Studio\23.0\bin\rsvars.bat" && msbuild src\EncodingFixTool.dproj /t:Build /p:Config=Debug /p:Platform=Win32'`
37+
Result: exit=0, zero warnings, zero errors
38+
- PASS: `cmd /s /c '"C:\Program Files (x86)\Embarcadero\Studio\23.0\bin\rsvars.bat" && msbuild tests\EncodingFixTool.Tests.dproj /t:Build /p:Config=Debug /p:Platform=Win32'`
39+
Result: exit=0, zero warnings, zero errors
40+
- PASS: `& 'F:\projects\MaxLogic\DelphiAiKit\bin\DelphiAIKit.exe' build --project src\EncodingFixTool.dproj --delphi 23.0 --platform Win32 --config Debug --target Build --ai`
41+
Result: success
42+
- PASS: `& 'F:\projects\MaxLogic\DelphiAiKit\bin\DelphiAIKit.exe' build --project tests\EncodingFixTool.Tests.dproj --delphi 23.0 --platform Win32 --config Debug --target Build --ai`
43+
Result: success
44+
- PASS: `.\bin\EncodingFixTool.exe path=. preset=delphi-ai scope=git-changed format=json`
45+
Result: exit=0, `failed=0`
46+
Touches: src/EncodingFixToolCore.pas, tests/EncodingFixTool.IntegrationTests.pas, tests/Invoke-EncodingFixTool.Tests.ps1, README.md, agent-skill/encodingfix-delphi-cleanup/SKILL.md, agent-skill/encodingfix-delphi-cleanup/references/encodingfix-tool.md, CHANGELOG.md
47+
Verify: integration-test, cli-proof, build
48+
Notes: Strict TDD followed for the DUnitX single-file regression. Initial RED was exit code 2 for `path=<file>`; GREEN passed after normalizing file paths into explicit single-file scan mode.
49+
2150
### T-006 [DOC] Refresh README for workflow and presets
2251
Completed: 2026-06-11
2352
Outcome:

agent-skill/encodingfix-delphi-cleanup/SKILL.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ If the executable is not on `PATH`, use a local Windows build such as:
5252
.\bin\EncodingFixTool.exe path=. preset=delphi-ai scope=git-changed format=json
5353
```
5454

55+
For one known file, use single-file mode instead of scanning the directory:
56+
57+
```powershell
58+
.\bin\EncodingFixTool.exe path=.\src\GeneratedUnit.pas preset=delphi-ai format=json
59+
```
60+
5561
3. Outside Git, use an explicit path and dry run first.
5662

5763
From WSL:
@@ -77,7 +83,7 @@ Treat the run as successful only when the process exits with code `0` and JSON `
7783
- `changed > 0`: inspect the relevant diffs before reporting done.
7884
- `skipped > 0`: usually expected for binary `.dfm`; mention it only when relevant.
7985
- `failed > 0` or exit code `1`: stop and report the file-level failure.
80-
- Exit code `2`: fix the missing `path` and rerun.
86+
- Exit code `2`: fix the missing directory or file `path` and rerun.
8187

8288
## Verify
8389

agent-skill/encodingfix-delphi-cleanup/references/encodingfix-tool.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ For a deliberately narrow repair, keep overrides visible:
4343
EncodingFixTool path=.\src preset=delphi-ai ext=pas,inc format=json dry
4444
```
4545

46+
For exactly one file, pass the file path directly. The tool treats the file's directory as the scan root and processes only that file:
47+
48+
```powershell
49+
EncodingFixTool path=.\src\GeneratedUnit.pas preset=delphi-ai format=json
50+
```
51+
4652
## Configured Presets
4753

4854
Repo presets may live in `.encodingfix.json`; user presets may live in:

src/EncodingFixTool.dproj

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,19 +90,19 @@
9090
<SanitizedProjectName>EncodingFixTool</SanitizedProjectName>
9191
<VerInfo_IncludeVerInfo>true</VerInfo_IncludeVerInfo>
9292
<VerInfo_MajorVer>1</VerInfo_MajorVer>
93-
<VerInfo_MinorVer>2</VerInfo_MinorVer>
93+
<VerInfo_MinorVer>3</VerInfo_MinorVer>
9494
<VerInfo_Release>0</VerInfo_Release>
9595
<VerInfo_Build>0</VerInfo_Build>
9696
<VerInfo_Locale>1045</VerInfo_Locale>
97-
<VerInfo_Keys>CompanyName=;FileDescription=;FileVersion=1.2.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=;ProductVersion=1.2.0.0;Comments=;CFBundleName=</VerInfo_Keys>
97+
<VerInfo_Keys>CompanyName=;FileDescription=;FileVersion=1.3.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=;ProductVersion=1.3.0.0;Comments=;CFBundleName=</VerInfo_Keys>
9898
<DCC_Namespace>System;Xml;Data;Datasnap;Web;Soap;$(DCC_Namespace)</DCC_Namespace>
9999
<Icon_MainIcon>$(BDS)\bin\delphi_PROJECTICON.ico</Icon_MainIcon>
100100
<Icns_MainIcns>$(BDS)\bin\delphi_PROJECTICNS.icns</Icns_MainIcns>
101101
<DCC_ExeOutput>..\bin</DCC_ExeOutput>
102102
<CfgDependentOn>..\..\MaxLogicFoundation\MaxLogicDefault.optset</CfgDependentOn>
103103
</PropertyGroup>
104104
<PropertyGroup Condition="'$(Base_Android)'!=''">
105-
<VerInfo_Keys>package=com.embarcadero.$(MSBuildProjectName);label=$(MSBuildProjectName);versionCode=1;versionName=1.2.0;persistent=False;restoreAnyVersion=False;installLocation=auto;largeHeap=False;theme=TitleBar;hardwareAccelerated=true;apiKey=;minSdkVersion=23;targetSdkVersion=34</VerInfo_Keys>
105+
<VerInfo_Keys>package=com.embarcadero.$(MSBuildProjectName);label=$(MSBuildProjectName);versionCode=1;versionName=1.3.0;persistent=False;restoreAnyVersion=False;installLocation=auto;largeHeap=False;theme=TitleBar;hardwareAccelerated=true;apiKey=;minSdkVersion=23;targetSdkVersion=34</VerInfo_Keys>
106106
<BT_BuildType>Debug</BT_BuildType>
107107
<Android_LauncherIcon36>$(BDS)\bin\Artwork\Android\FM_LauncherIcon_36x36.png</Android_LauncherIcon36>
108108
<Android_LauncherIcon48>$(BDS)\bin\Artwork\Android\FM_LauncherIcon_48x48.png</Android_LauncherIcon48>
@@ -141,13 +141,13 @@
141141
<PropertyGroup Condition="'$(Base_Win32)'!=''">
142142
<DCC_Namespace>Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;$(DCC_Namespace)</DCC_Namespace>
143143
<BT_BuildType>Debug</BT_BuildType>
144-
<VerInfo_Keys>CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.2.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=$(MSBuildProjectName);ProductVersion=1.2.0.0;Comments=;ProgramID=com.embarcadero.$(MSBuildProjectName)</VerInfo_Keys>
144+
<VerInfo_Keys>CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.3.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=$(MSBuildProjectName);ProductVersion=1.3.0.0;Comments=;ProgramID=com.embarcadero.$(MSBuildProjectName)</VerInfo_Keys>
145145
<VerInfo_Locale>1033</VerInfo_Locale>
146146
</PropertyGroup>
147147
<PropertyGroup Condition="'$(Base_Win64)'!=''">
148148
<DCC_Namespace>Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;$(DCC_Namespace)</DCC_Namespace>
149149
<BT_BuildType>Debug</BT_BuildType>
150-
<VerInfo_Keys>CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.2.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=$(MSBuildProjectName);ProductVersion=1.2.0.0;Comments=;ProgramID=com.embarcadero.$(MSBuildProjectName)</VerInfo_Keys>
150+
<VerInfo_Keys>CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.3.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=$(MSBuildProjectName);ProductVersion=1.3.0.0;Comments=;ProgramID=com.embarcadero.$(MSBuildProjectName)</VerInfo_Keys>
151151
<VerInfo_Locale>1033</VerInfo_Locale>
152152
</PropertyGroup>
153153
<PropertyGroup Condition="'$(Cfg_1)'!=''">
@@ -177,7 +177,7 @@
177177
</PropertyGroup>
178178
<PropertyGroup Condition="'$(Cfg_2_Win32)'!=''">
179179
<VerInfo_Locale>1033</VerInfo_Locale>
180-
<VerInfo_Keys>CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.2.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=$(MSBuildProjectName);ProductVersion=1.2.0.0;Comments=;ProgramID=com.embarcadero.$(MSBuildProjectName)</VerInfo_Keys>
180+
<VerInfo_Keys>CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.3.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=$(MSBuildProjectName);ProductVersion=1.3.0.0;Comments=;ProgramID=com.embarcadero.$(MSBuildProjectName)</VerInfo_Keys>
181181
<Manifest_File>(None)</Manifest_File>
182182
<AppDPIAwarenessMode>none</AppDPIAwarenessMode>
183183
</PropertyGroup>

src/EncodingFixTool.res

0 Bytes
Binary file not shown.

src/EncodingFixToolCore.pas

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ TOptions = record
3737
Silent: boolean;
3838
Verbose: boolean; // not compatible with Silent
3939
Path: string;
40+
SingleFilePath: string;
4041
Recursive: boolean; // default y
4142
Exts: TArray<string>; // normalized: ".pas", ".dpr", etc.
4243
Utf8Bom: boolean; // default y
@@ -214,6 +215,17 @@ function TEncodingFixTool.CollectFiles(const aOptions: TOptions): TArray<string>
214215
lSearchOpt: TSearchOption;
215216
lExt, lPattern: string;
216217
begin
218+
if aOptions.SingleFilePath <> '' then
219+
begin
220+
if IsWantedExt(aOptions.SingleFilePath) then
221+
begin
222+
Result := [aOptions.SingleFilePath];
223+
end else begin
224+
Result := [];
225+
end;
226+
Exit;
227+
end;
228+
217229
if aOptions.ScopeMode = TScopeMode.smGitChanged then
218230
begin
219231
Exit(CollectGitChangedFiles(aOptions));
@@ -601,7 +613,6 @@ function TEncodingFixTool.DecodeBestPerLine(const aLineBytes: TBytes; out aEncNa
601613
sc := ScoreDecoded(sANSI);
602614
if sc > bestScore then
603615
begin
604-
bestScore := sc;
605616
bestS := sANSI;
606617
aEncName := 'ANSI';
607618
end;
@@ -1074,7 +1085,7 @@ function TEncodingFixTool.ShowHelp: integer;
10741085
' dry : Dry run (no files are changed).' + sLineBreak +
10751086
' s | silent : No console output.' + sLineBreak +
10761087
' v | verbose : More output (not compatible with silent).' + sLineBreak +
1077-
' path=<dir> : Directory to scan. Default: current working dir.' + sLineBreak +
1088+
' path=<dir|file> : Directory to scan or one file to process. Default: current working dir.' + sLineBreak +
10781089
' recursive=y|n : Recurse into subfolders. Default: y.' + sLineBreak +
10791090
' ext=<csv> : Extensions list. Default: pas,dpr. Accepts "pas", ".pas", "*.pas".' + sLineBreak +
10801091
' preset=delphi-ai : AI cleanup preset for Delphi projects.' + sLineBreak +
@@ -1208,7 +1219,15 @@ function TEncodingFixTool.ParseCommandLine(out aOptions: TOptions): integer;
12081219
begin
12091220
if lValue <> '' then
12101221
begin
1211-
aOptions.Path := ExpandFileName(lValue);
1222+
lValue := ExpandFileName(lValue);
1223+
aOptions.SingleFilePath := '';
1224+
if TFile.Exists(lValue) then
1225+
begin
1226+
aOptions.SingleFilePath := lValue;
1227+
aOptions.Path := ExtractFilePath(lValue);
1228+
end else begin
1229+
aOptions.Path := lValue;
1230+
end;
12121231
end;
12131232
end else if (aKey = 'recursive') then
12141233
begin
@@ -1393,6 +1412,7 @@ function TEncodingFixTool.ParseCommandLine(out aOptions: TOptions): integer;
13931412
aOptions.Silent := False;
13941413
aOptions.Verbose := False;
13951414
aOptions.Path := GetCurrentDir;
1415+
aOptions.SingleFilePath := '';
13961416
aOptions.Recursive := True;
13971417
aOptions.Exts := NormalizeExtList('pas,dpr');
13981418
aOptions.Utf8Bom := True; // default y (Delphi-friendly)

tests/EncodingFixTool.IntegrationTests.pas

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ TEncodingFixToolIntegrationTests = class
7373
[Category('AiWorkflow')]
7474
procedure GitChangedScopeProcessesOnlyChangedFiles;
7575

76+
[Test]
77+
[Category('SingleFile')]
78+
procedure FilePathProcessesOnlyThatFile;
79+
7680
[Test]
7781
[Category('PresetConfig')]
7882
procedure ExplicitConfigPresetAppliesToIncludedExtensions;
@@ -573,6 +577,27 @@ procedure TEncodingFixToolIntegrationTests.GitChangedScopeProcessesOnlyChangedFi
573577
end;
574578
end;
575579

580+
procedure TEncodingFixToolIntegrationTests.FilePathProcessesOnlyThatFile;
581+
var
582+
lFileName: string;
583+
lOtherFileName: string;
584+
lRootPath: string;
585+
begin
586+
lRootPath := TPath.Combine(TPath.GetTempPath, 'EncodingFix-DUnitX-' + TGuid.NewGuid.ToString);
587+
TDirectory.CreateDirectory(lRootPath);
588+
try
589+
lFileName := WriteBytes(lRootPath, 'target.pas', TEncoding.ASCII.GetBytes('unit Target;'#10'end.'));
590+
lOtherFileName := WriteBytes(lRootPath, 'other.pas', TEncoding.ASCII.GetBytes('unit Other;'#10'end.'));
591+
592+
Assert.AreEqual(0, Integer(RunTool(lFileName, ['recursive=n', 'ext=pas', 'eol=crlf', 's'])));
593+
594+
AssertBytesEqual(TEncoding.ASCII.GetBytes('unit Target;'#13#10'end.'), TFile.ReadAllBytes(lFileName));
595+
AssertBytesEqual(TEncoding.ASCII.GetBytes('unit Other;'#10'end.'), TFile.ReadAllBytes(lOtherFileName));
596+
finally
597+
DeleteTree(lRootPath);
598+
end;
599+
end;
600+
576601
procedure TEncodingFixToolIntegrationTests.ExplicitConfigPresetAppliesToIncludedExtensions;
577602
var
578603
lFileName: string;

0 commit comments

Comments
 (0)