program ExtractPascalInterfaces; {$APPTYPE CONSOLE} uses System.SysUtils, System.IOUtils, System.Classes, System.Generics.Collections, System.Generics.Defaults, System.StrUtils, System.Types, System.Character, Winapi.Windows; // For clipboard access type { Forward declaration for TUnitInfoList needed in TUnitInfo } TUnitInfo = class; TUnitInfoList = TList; { TUnitInfo holds information about a single Pascal unit } TUnitInfo = class private FOriginalLines: TStringList; procedure ExtractUsesFromClause(const Clause: string); function StripCommentsFromUsesBlock(const Block: string): string; function FindTerminatingSemicolon(const Block: string): Integer; function CheckAndExtractKeywordContent( const LineInput: string; const KeywordUpperCase: string; out ContentAfterKeyword: string ): Boolean; public Name: string; NormalizedName: string; FilePath: string; InterfaceSection: TStringList; InterfaceUses: TStringList; AdjacencyList: TUnitInfoList; InDegree: Integer; Processed: Boolean; constructor Create(const AFilePath: string); destructor Destroy; override; function ParseUnit: Boolean; end; const DefaultExcludedDirNames: array[0..6] of string = ('.svn', '.git', '.hg', '__history', '__recovery', '.vs', '.vscode'); // Helper procedure to set clipboard text using WinAPI procedure SetClipboardText(const Text: string); var hGlobal: THandle; pGlobal: Pointer; begin if not OpenClipboard(0) then RaiseLastOSError; try EmptyClipboard; hGlobal := GlobalAlloc(GMEM_MOVEABLE, (Length(Text) + 1) * SizeOf(Char)); if hGlobal = 0 then RaiseLastOSError; pGlobal := GlobalLock(hGlobal); if pGlobal = nil then begin GlobalFree(hGlobal); RaiseLastOSError; end; try Move(PChar(Text)^, pGlobal^, (Length(Text) + 1) * SizeOf(Char)); finally GlobalUnlock(hGlobal); end; if SetClipboardData(CF_UNICODETEXT, hGlobal) = 0 then begin GlobalFree(hGlobal); // We must free it if SetClipboardData fails RaiseLastOSError; end; // On success, the system owns the memory handle. finally CloseClipboard; end; end; { TUnitInfo implementation } constructor TUnitInfo.Create(const AFilePath: string); begin inherited Create; FilePath := AFilePath; Name := ''; NormalizedName := ''; InterfaceSection := TStringList.Create; InterfaceUses := TStringList.Create; FOriginalLines := TStringList.Create; AdjacencyList := TUnitInfoList.Create; InDegree := 0; Processed := False; end; destructor TUnitInfo.Destroy; begin InterfaceSection.Free; InterfaceUses.Free; FOriginalLines.Free; AdjacencyList.Free; inherited Destroy; end; function TUnitInfo.StripCommentsFromUsesBlock(const Block: string): string; var Builder: TStringBuilder; i: Integer; L: Integer; inLineComment: Boolean; inBlockCommentParen: Boolean; inBlockCommentBrace: Boolean; currentChar, nextChar: Char; tempResult: string; begin Builder := TStringBuilder.Create; L := Length(Block); i := 1; inLineComment := False; inBlockCommentParen := False; inBlockCommentBrace := False; while i <= L do begin currentChar := Block[i]; if i < L then nextChar := Block[i + 1] else nextChar := #0; if inLineComment then begin if (currentChar = #13) or (currentChar = #10) then begin inLineComment := False; if (Builder.Length > 0) and (Builder[Builder.Length - 1] <> ' ') and (Builder[Builder.Length - 1] <> ',') then Builder.Append(' '); end; Inc(i); Continue; end; if inBlockCommentParen then begin if (currentChar = '*') and (nextChar = ')') then begin inBlockCommentParen := False; Inc(i, 2); end else Inc(i); Continue; end; if inBlockCommentBrace then begin if currentChar = '}' then begin inBlockCommentBrace := False; Inc(i); end else Inc(i); Continue; end; if (currentChar = '/') and (nextChar = '/') then begin inLineComment := True; Inc(i, 2); Continue; end; if (currentChar = '(') and (nextChar = '*') then begin inBlockCommentParen := True; Inc(i, 2); Continue; end; if currentChar = '{' then begin inBlockCommentBrace := True; Inc(i); Continue; end; if (currentChar = #13) or (currentChar = #10) or (currentChar = #9) then begin if (Builder.Length = 0) or ((Builder.Length > 0) and (Builder[Builder.Length - 1] <> ' ')) then Builder.Append(' '); Inc(i); end else begin Builder.Append(currentChar); Inc(i); end; end; tempResult := Trim(Builder.ToString); Builder.Free; while Pos(' ', tempResult) > 0 do tempResult := StringReplace(tempResult, ' ', ' ', [rfReplaceAll]); tempResult := StringReplace(tempResult, ' ,', ',', [rfReplaceAll]); tempResult := StringReplace(tempResult, ', ', ',', [rfReplaceAll]); Result := tempResult; end; procedure TUnitInfo.ExtractUsesFromClause(const Clause: string); var parts: TArray; partName: string; begin if Trim(Clause) = '' then Exit; parts := Clause.Split([','], TStringSplitOptions.ExcludeEmpty); for partName in parts do begin var pn := Trim(partName); if pn <> '' then InterfaceUses.Add(System.SysUtils.LowerCase(pn)); end; end; function TUnitInfo.FindTerminatingSemicolon(const Block: string): Integer; var i: Integer; L: Integer; inLineComment: Boolean; inBlockCommentParen: Boolean; inBlockCommentBrace: Boolean; currentChar: Char; nextChar: Char; begin Result := 0; L := Length(Block); i := 1; inLineComment := False; inBlockCommentParen := False; inBlockCommentBrace := False; while i <= L do begin currentChar := Block[i]; if i < L then nextChar := Block[i + 1] else nextChar := #0; if inLineComment then begin if (currentChar = #13) or (currentChar = #10) then inLineComment := False; Inc(i); Continue; end; if inBlockCommentParen then begin if (currentChar = '*') and (nextChar = ')') then begin inBlockCommentParen := False; Inc(i, 2); end else Inc(i); Continue; end; if inBlockCommentBrace then begin if currentChar = '}' then begin inBlockCommentBrace := False; Inc(i); end else Inc(i); Continue; end; if (currentChar = '/') and (nextChar = '/') then begin inLineComment := True; Inc(i, 2); Continue; end; if (currentChar = '(') and (nextChar = '*') then begin inBlockCommentParen := True; Inc(i, 2); Continue; end; if currentChar = '{' then begin inBlockCommentBrace := True; Inc(i); Continue; end; if currentChar = ';' then begin Result := i; Exit; end; Inc(i); end; end; function TUnitInfo.CheckAndExtractKeywordContent( const LineInput: string; const KeywordUpperCase: string; out ContentAfterKeyword: string ): Boolean; var trimmedLine: string; keywordLen: Integer; charAfterKeyword: Char; begin Result := False; ContentAfterKeyword := ''; trimmedLine := Trim(LineInput); keywordLen := Length(KeywordUpperCase); if Length(trimmedLine) >= keywordLen then begin if System.SysUtils.UpperCase(Copy(trimmedLine, 1, keywordLen)) = KeywordUpperCase then begin if Length(trimmedLine) = keywordLen then begin Result := True; ContentAfterKeyword := ''; end else begin charAfterKeyword := trimmedLine[keywordLen + 1]; if not charAfterKeyword.IsLetterOrDigit then begin Result := True; ContentAfterKeyword := Trim(Copy(trimmedLine, keywordLen + 1, MaxInt)); end; end; end; end; end; function TUnitInfo.ParseUnit: Boolean; var i: Integer; LineValue: string; upperLine: string; inInterface: Boolean; inUsesBlock: Boolean; usesBuilder: TStringBuilder; potentialUnitNameLine: string; nameExtracted: Boolean; contentAfterKeyword: string; accumulatedContent: string; trueEndOfUsesPos: Integer; contentToClean: string; cleanedContent: string; begin Result := False; if not TFile.Exists(FilePath) then exit; FOriginalLines.LoadFromFile(FilePath); inInterface := False; inUsesBlock := False; nameExtracted := False; usesBuilder := TStringBuilder.Create; try for i := 0 to FOriginalLines.Count - 1 do begin LineValue := FOriginalLines[i]; upperLine := System.SysUtils.UpperCase(Trim(LineValue)); if not nameExtracted and CheckAndExtractKeywordContent(LineValue, 'UNIT', contentAfterKeyword) then begin potentialUnitNameLine := contentAfterKeyword; if Pos(';', potentialUnitNameLine) > 0 then Name := Trim(Copy(potentialUnitNameLine, 1, Pos(';', potentialUnitNameLine) - 1)) else Name := Trim(potentialUnitNameLine); if Name <> '' then begin NormalizedName := System.SysUtils.LowerCase(Name); nameExtracted := True; end else begin WriteLn('Warning: Could not parse unit name from: ' + LineValue + ' in file ' + FilePath); exit; end; end; if not inInterface and upperLine.StartsWith('INTERFACE') then begin inInterface := True; end; if inInterface then begin if upperLine.StartsWith('IMPLEMENTATION') or upperLine.StartsWith('END.') then begin if inUsesBlock and (usesBuilder.Length > 0) then begin accumulatedContent := usesBuilder.ToString; trueEndOfUsesPos := FindTerminatingSemicolon(accumulatedContent); if trueEndOfUsesPos > 0 then contentToClean := Trim(Copy(accumulatedContent, 1, trueEndOfUsesPos - 1)) else contentToClean := Trim(accumulatedContent); if contentToClean <> '' then begin cleanedContent := StripCommentsFromUsesBlock(contentToClean); if Trim(cleanedContent) <> '' then ExtractUsesFromClause(cleanedContent); end; end; Result := True; exit; end; var lineIsForUses := inUsesBlock; if not inUsesBlock and CheckAndExtractKeywordContent(LineValue, 'USES', contentAfterKeyword) then begin usesBuilder.Clear; inUsesBlock := True; usesBuilder.AppendLine(contentAfterKeyword); // Use AppendLine to preserve structure for comment stripping lineIsForUses := True; end else if inUsesBlock then begin // FIXED: Use AppendLine to preserve newlines for comment stripping logic usesBuilder.AppendLine(Trim(LineValue)); lineIsForUses := True; end; if not upperLine.StartsWith('INTERFACE') and not lineIsForUses then begin InterfaceSection.Add(LineValue); end; if inUsesBlock then begin accumulatedContent := usesBuilder.ToString; trueEndOfUsesPos := FindTerminatingSemicolon(accumulatedContent); if trueEndOfUsesPos > 0 then begin contentToClean := Trim(Copy(accumulatedContent, 1, trueEndOfUsesPos - 1)); usesBuilder.Clear; inUsesBlock := False; if contentToClean <> '' then begin cleanedContent := StripCommentsFromUsesBlock(contentToClean); if Trim(cleanedContent) <> '' then ExtractUsesFromClause(cleanedContent); end; end; end; end; end; if inInterface then begin Result := True; end; finally usesBuilder.Free; end; end; procedure CollectPasFilesRecursively(const Path: string; Files: TStringList; const ExcludedDirPatterns: TStrings); var dirName: string; foundPasFiles: TStringDynArray; subDirs: TStringDynArray; i: Integer; currentSubDir: string; isExcluded: Boolean; exclusionPattern: string; begin try foundPasFiles := TDirectory.GetFiles(Path, '*.pas', TSearchOption.soTopDirectoryOnly); for i := Low(foundPasFiles) to High(foundPasFiles) do Files.Add(foundPasFiles[i]); except on E: EDirectoryNotFoundException do begin WriteLn('Warning: Could not access path ' + Path + ' while scanning files: ' + E.Message); Exit; end; on E: EInOutError do begin WriteLn('Warning: IO error accessing path ' + Path + ' while scanning files: ' + E.Message); Exit; end; end; try subDirs := TDirectory.GetDirectories(Path); except on E: EDirectoryNotFoundException do begin WriteLn('Warning: Could not access path ' + Path + ' while scanning subdirectories: ' + E.Message); Exit; end; on E: EInOutError do begin WriteLn('Warning: IO error accessing path ' + Path + ' while scanning subdirectories: ' + E.Message); Exit; end; end; for currentSubDir in subDirs do begin dirName := ExtractFileName(currentSubDir); isExcluded := False; for exclusionPattern in ExcludedDirPatterns do begin if SameText(dirName, exclusionPattern) then begin isExcluded := True; Break; end; end; if not isExcluded then CollectPasFilesRecursively(currentSubDir, Files, ExcludedDirPatterns); end; end; var allUnits: TDictionary; sortedUnits: TList; sourceDirectories: TList; processingQueue: TQueue; excludedDirectoryPatternsList: TStringList; allFoundPasFilesList: TStringList; item: TUnitInfo; outputLineText: string; recursiveSearch: Boolean; outputFileName: string; writeToClipboard: Boolean; currentParamIndex: Integer; focusMode: Boolean; focusFilesParameter: string; initialFocusFiles: TStringList; filesToExcludeFromOutput: TStringList; begin recursiveSearch := False; outputFileName := ''; writeToClipboard := False; sourceDirectories := nil; allUnits := nil; sortedUnits := nil; processingQueue := nil; excludedDirectoryPatternsList := nil; allFoundPasFilesList := nil; initialFocusFiles := nil; filesToExcludeFromOutput := nil; exitcode := 0; focusMode := False; focusFilesParameter := ''; try var usageStr := 'Usage: ExtractPascalInterfaces.exe [-r] [-dirs ] [-f | -fx ] [-o | -c] ...'; WriteLn('Delphi Interface Extractor'); sourceDirectories := TList.Create; excludedDirectoryPatternsList := TStringList.Create; initialFocusFiles := TStringList.Create; filesToExcludeFromOutput := TStringList.Create; for var excludedName in DefaultExcludedDirNames do excludedDirectoryPatternsList.Add(excludedName); currentParamIndex := 1; while currentParamIndex <= ParamCount do begin var currentParam := ParamStr(currentParamIndex); if SameText(currentParam, '-r') then begin recursiveSearch := True; Inc(currentParamIndex); end else if SameText(currentParam, '-c') then begin writeToClipboard := True; Inc(currentParamIndex); end else if SameText(currentParam, '-o') then begin if currentParamIndex + 1 <= ParamCount then begin outputFileName := ParamStr(currentParamIndex + 1); Inc(currentParamIndex, 2); end else begin WriteLn('Error: Missing filename after -o option.'); WriteLn(usageStr); exitcode := 1; exit; end; end else if SameText(currentParam, '-dirs') then begin if currentParamIndex + 1 <= ParamCount then begin var dirsFileName := ParamStr(currentParamIndex + 1); if TFile.Exists(dirsFileName) then begin var dirsFromFile := TStringList.Create; try dirsFromFile.LoadFromFile(dirsFileName); for var dirPath in dirsFromFile do sourceDirectories.Add(dirPath); finally dirsFromFile.Free; end; end else WriteLn('Warning: File specified with -dirs not found: ' + dirsFileName); Inc(currentParamIndex, 2); end else begin WriteLn('Error: Missing filename after -dirs option.'); WriteLn(usageStr); exitcode := 1; exit; end; end else if SameText(currentParam, '-f') or SameText(currentParam, '-fx') then begin if focusMode then begin WriteLn('Error: -f and -fx options cannot be used together.'); WriteLn(usageStr); exitcode := 1; exit; end; if currentParamIndex + 1 <= ParamCount then begin focusMode := True; focusFilesParameter := ParamStr(currentParamIndex + 1); initialFocusFiles.Text := StringReplace(focusFilesParameter, ';', sLineBreak, [rfReplaceAll]); if SameText(currentParam, '-fx') then filesToExcludeFromOutput.Text := initialFocusFiles.Text; Inc(currentParamIndex, 2); end else begin WriteLn('Error: Missing file list after ' + currentParam + ' option.'); WriteLn(usageStr); exitcode := 1; exit; end; end else begin sourceDirectories.Add(currentParam); inc(currentParamIndex); end; end; var i := sourceDirectories.Count - 1; while i >= 0 do begin var trimmedPath := Trim(sourceDirectories[i]); if (trimmedPath = '') or (not TDirectory.Exists(trimmedPath)) then begin if trimmedPath <> '' then WriteLn('Warning: Directory "' + trimmedPath + '" not found or is not a directory. Skipping.'); sourceDirectories.Delete(i); end else sourceDirectories[i] := trimmedPath; Dec(i); end; if writeToClipboard and (outputFileName <> '') then begin WriteLn('Error: -o (output to file) and -c (output to clipboard) cannot be used together.'); exitcode := 1; exit; end; if sourceDirectories.Count = 0 then begin WriteLn('Error: No source directories specified.'); WriteLn(usageStr); exitcode := 1; exit; end; allFoundPasFilesList := TStringList.Create; WriteLn(IfThen(recursiveSearch, 'Recursive search enabled.', 'Non-recursive search.')); for var currentScanDir in sourceDirectories do begin WriteLn('Scanning directory: ' + currentScanDir + IfThen(recursiveSearch, ' (recursively)', '')); if recursiveSearch then CollectPasFilesRecursively(currentScanDir, allFoundPasFilesList, excludedDirectoryPatternsList) else begin var filesInDir := TDirectory.GetFiles(currentScanDir, '*.pas', TSearchOption.soTopDirectoryOnly); for var fName in filesInDir do allFoundPasFilesList.Add(fName); end; end; if focusMode then begin for var focusFile in initialFocusFiles do begin if not TFile.Exists(focusFile) then begin WriteLn('Warning: Focus file not found, skipping: ' + focusFile); Continue; end; if allFoundPasFilesList.IndexOf(focusFile) = -1 then allFoundPasFilesList.Add(focusFile); end; end; if allFoundPasFilesList.Count = 0 then begin WriteLn('No .pas files found in any specified directory (respecting exclusions).'); exit; end; allUnits := TDictionary.Create(TStringComparer.Ordinal); sortedUnits := TList.Create; processingQueue := TQueue.Create; WriteLn('Found ' + IntToStr(allFoundPasFilesList.Count) + ' .pas files in total. Parsing...'); for var filePathKey in allFoundPasFilesList do begin var unitInfo := TUnitInfo.Create(filePathKey); if unitInfo.ParseUnit then begin if unitInfo.Name <> '' then begin if not allUnits.ContainsKey(unitInfo.NormalizedName) then allUnits.Add(unitInfo.NormalizedName, unitInfo) else begin WriteLn('Warning: Duplicate unit name "' + unitInfo.Name + '" found.'); WriteLn(' Existing: ' + allUnits[unitInfo.NormalizedName].FilePath); WriteLn(' Skipped: ' + unitInfo.FilePath); unitInfo.Free; end; end else begin WriteLn('Warning: Could not determine unit name for file: ' + filePathKey + '. Skipping.'); unitInfo.Free; end; end else begin WriteLn('Warning: Failed to parse unit: ' + filePathKey + '. Skipping.'); unitInfo.Free; end; end; if allUnits.Count = 0 then begin WriteLn('No units successfully parsed.'); exit; end; if focusMode then begin WriteLn('Focus mode active. Finding dependency cone for files: ' + focusFilesParameter); var workQueue := TQueue.Create; var requiredUnits := TDictionary.Create(TStringComparer.Ordinal); var unitsToRemove := TStringList.Create; try for var unitInfo in allUnits.Values do begin if initialFocusFiles.IndexOf(unitInfo.FilePath) > -1 then begin if not requiredUnits.ContainsKey(unitInfo.NormalizedName) then begin workQueue.Enqueue(unitInfo); requiredUnits.Add(unitInfo.NormalizedName, unitInfo); end; end; end; if requiredUnits.Count = 0 then WriteLn('Warning: None of the initial focus files could be parsed successfully.'); while workQueue.Count > 0 do begin var currentUnit := workQueue.Dequeue; for var usedUnitName in currentUnit.InterfaceUses do begin var dependencyUnit: TUnitInfo; if allUnits.TryGetValue(usedUnitName, dependencyUnit) and (not requiredUnits.ContainsKey(usedUnitName)) then begin requiredUnits.Add(dependencyUnit.NormalizedName, dependencyUnit); workQueue.Enqueue(dependencyUnit); end; end; end; WriteLn('Found ' + requiredUnits.Count.ToString + ' required units in total.'); for var unitInfo in allUnits.Values do if not requiredUnits.ContainsKey(unitInfo.NormalizedName) then unitsToRemove.Add(unitInfo.NormalizedName); if unitsToRemove.Count > 0 then begin WriteLn('Filtering out ' + unitsToRemove.Count.ToString + ' unneeded units...'); for var unitNameToRemove in unitsToRemove do begin allUnits[unitNameToRemove].Free; allUnits.Remove(unitNameToRemove); end; end; finally workQueue.Free; requiredUnits.Free; unitsToRemove.Free; end; end; WriteLn('Building dependency graph...'); for var currentUnit in allUnits.Values do for var usedUnitName in currentUnit.InterfaceUses do begin var usedUnitInfo: TUnitInfo; if allUnits.TryGetValue(usedUnitName, usedUnitInfo) then begin currentUnit.InDegree := currentUnit.InDegree + 1; usedUnitInfo.AdjacencyList.Add(currentUnit); end; end; WriteLn(''); WriteLn('--------------------------------------------------------------------------------'); WriteLn('Dependency Graph (Project Units Only):'); WriteLn('--------------------------------------------------------------------------------'); if allUnits.Count = 0 then WriteLn('(No units to display in graph)') else begin var tempUnitListForDisplay: TList; tempUnitListForDisplay := TList.Create(allUnits.Values); try tempUnitListForDisplay.Sort( TComparer .Construct(function(const L, R: TUnitInfo): Integer begin Result := CompareText(L.Name, R.Name); end) ); for var unitToDisplay in tempUnitListForDisplay do begin var usesClauseText := ''; var projectDependenciesFound := False; if unitToDisplay.InterfaceUses.Count > 0 then begin var usedUnitDisplayNames := TStringList.Create; try for var usedNormalizedName in unitToDisplay.InterfaceUses do begin var usedUnitActualInfo: TUnitInfo; if allUnits.TryGetValue(usedNormalizedName, usedUnitActualInfo) then begin usedUnitDisplayNames.Add(usedUnitActualInfo.Name); projectDependenciesFound := True; end; end; if projectDependenciesFound then usesClauseText := usedUnitDisplayNames.CommaText else usesClauseText := '(none)'; finally usedUnitDisplayNames.Free; end; end else usesClauseText := '(none)'; WriteLn(Format(' %-40s -> %s', [unitToDisplay.Name, usesClauseText])); end; finally tempUnitListForDisplay.Free; end; end; WriteLn('--------------------------------------------------------------------------------'); WriteLn(''); WriteLn('Performing topological sort...'); for item in allUnits.Values do if item.InDegree = 0 then processingQueue.Enqueue(item); while processingQueue.Count > 0 do begin var u := processingQueue.Dequeue; sortedUnits.Add(u); u.Processed := True; for var v in u.AdjacencyList do begin v.InDegree := v.InDegree - 1; if (v.InDegree = 0) and (not v.Processed) then processingQueue.Enqueue(v); end; end; if sortedUnits.Count < allUnits.Count then begin WriteLn('Error: Circular dependency detected among units. Output generation aborted.'); WriteLn('Units not processed:'); for item in allUnits.Values do if not item.Processed then WriteLn('- ' + item.Name + ' (InDegree: ' + item.InDegree.ToString + ', File: ' + item.FilePath + ')'); exitcode := 1; end else begin var outputContent := TStringList.Create; var externalUses := TDictionary.Create; try outputContent.Add('// Combined interfaces from directories:'); for var dirPath in sourceDirectories do outputContent.Add('// - ' + dirPath + IfThen(recursiveSearch, ' (recursive)', '')); if focusMode then begin var modeStr := IfThen(filesToExcludeFromOutput.Count > 0, '-fx', '-f'); outputContent.Add('// Focus mode (' + modeStr + ') active for files: ' + focusFilesParameter); end; outputContent.Add('// Generated on: ' + DateTimeToStr(Now)); outputContent.Add(''); for item in sortedUnits do begin if filesToExcludeFromOutput.IndexOf(item.FilePath) > -1 then Continue; for var usedUnit in item.InterfaceUses do if not allUnits.ContainsKey(usedUnit) then externalUses.AddOrSetValue(usedUnit, True); end; if externalUses.Count > 0 then begin var externalUsesList := TStringList.Create; try for var extUse in externalUses.Keys do externalUsesList.Add(extUse); externalUsesList.Sort; outputContent.Add('uses'); for var j := 0 to externalUsesList.Count - 1 do begin if j = externalUsesList.Count - 1 then outputContent.Add(' ' + externalUsesList[j] + ';') else outputContent.Add(' ' + externalUsesList[j] + ','); end; outputContent.Add(''); finally externalUsesList.Free; end; end; for item in sortedUnits do begin if filesToExcludeFromOutput.IndexOf(item.FilePath) > -1 then Continue; if item.InterfaceSection.Count > 0 then begin outputContent .Add('//=================================================================================================='); outputContent.Add('//== UNIT START: ' + item.Name + ' (from ' + ExtractFileName(item.FilePath) + ')'); outputContent .Add('//=================================================================================================='); outputContent.AddStrings(item.InterfaceSection); outputContent.Add('//== UNIT END: ' + item.Name); outputContent .Add('//=================================================================================================='); outputContent.Add(''); end; end; if writeToClipboard then begin SetClipboardText(outputContent.Text); WriteLn('Output successfully copied to clipboard.'); end else if outputFileName <> '' then begin outputContent.SaveToFile(outputFileName); WriteLn('Output successfully written to: ' + outputFileName); end else begin for outputLineText in outputContent do WriteLn(outputLineText); end; finally outputContent.Free; externalUses.Free; end; end; except on E: Exception do begin WriteLn('An error occurred: ' + E.ClassName + ': ' + E.Message); exitcode := 1; end; end; if Assigned(filesToExcludeFromOutput) then filesToExcludeFromOutput.Free; if Assigned(initialFocusFiles) then initialFocusFiles.Free; if Assigned(excludedDirectoryPatternsList) then excludedDirectoryPatternsList.Free; if Assigned(allFoundPasFilesList) then allFoundPasFilesList.Free; if Assigned(sourceDirectories) then sourceDirectories.Free; if Assigned(allUnits) then begin for item in allUnits.Values do item.Free; allUnits.Free; end; if Assigned(sortedUnits) then sortedUnits.Free; if Assigned(processingQueue) then processingQueue.Free; if exitcode <> 0 then WriteLn('Extraction failed.'); end.