diff --git a/Build/Build-Module.ps1 b/Build/Build-Module.ps1 index 06fb25da..0a4d4eec 100644 --- a/Build/Build-Module.ps1 +++ b/Build/Build-Module.ps1 @@ -2,13 +2,12 @@ # since PSD1 is not required for proper rebuilding, we use PSM1 for this module only # most modules should be run via PSD1 or by it's name (which in the background uses PD1) -# This version is used for GitHub Actions and is used to build the module - +# This version is for local building # We need to rmeove library before we start, as it may contain old files, which will be in use once PSD1 loads # This is only required for PSPublisModule, as it's the only module that is being built by itself Remove-Item -Path "C:\Support\GitHub\PSPublishModule\Lib" -Recurse -Force -ErrorAction SilentlyContinue -Import-Module ([io.path]::Combine($PSScriptRoot, '..', 'PSPublishModule.psd1')) -Force +Import-Module "$PSScriptRoot\..\PSPublishModule.psd1" -Force Build-Module -ModuleName 'PSPublishModule' { # Usual defaults as per standard module @@ -61,9 +60,9 @@ Build-Module -ModuleName 'PSPublishModule' { 'New-FileCatalog' ) - $ConfigurationFormat = [ordered] @{ - RemoveComments = $false + RemoveComments = $true + RemoveEmptyLines = $true PlaceOpenBraceEnable = $true PlaceOpenBraceOnSameLine = $true @@ -71,7 +70,7 @@ Build-Module -ModuleName 'PSPublishModule' { PlaceOpenBraceIgnoreOneLineBlock = $false PlaceCloseBraceEnable = $true - PlaceCloseBraceNewLineAfter = $false + PlaceCloseBraceNewLineAfter = $true PlaceCloseBraceIgnoreOneLineBlock = $false PlaceCloseBraceNoEmptyLineBefore = $true @@ -103,30 +102,30 @@ Build-Module -ModuleName 'PSPublishModule' { New-ConfigurationFormat -ApplyTo 'DefaultPSD1', 'OnMergePSD1' -PSD1Style 'Minimal' # configuration for documentation, at the same time it enables documentation processing - New-ConfigurationDocumentation -Enable:$true -StartClean -UpdateWhenNew -PathReadme ([io.path]::Combine('Docs', 'Readme.md')) -Path 'Docs' -Tool HelpOut + New-ConfigurationDocumentation -Enable:$true -StartClean -UpdateWhenNew -PathReadme 'Docs\Readme.md' -Path 'Docs' New-ConfigurationImportModule -ImportSelf $newConfigurationBuildSplat = @{ Enable = $true - # temporary not signing - SignModule = $false + SignModule = if ($Env:COMPUTERNAME -eq 'EVOMONSTER') { $true } else { $false } DeleteTargetModuleBeforeBuild = $true MergeModuleOnBuild = $true - CertificateThumbprint = '' + CertificateThumbprint = '483292C9E317AA13B07BB7A96AE9D1A5ED9E7703' #CertificatePFXBase64 = $BasePfx #CertificatePFXPassword = "zGT" DoNotAttemptToFixRelativePaths = $false SkipBuiltinReplacements = $true # required for Cmdlet/Alias functionality - NETProjectPath = [io.path]::Combine($PSScriptRoot, '..', 'Sources', 'PSPublishModule') + NETProjectPath = "$PSScriptRoot\..\Sources\PSPublishModule" ResolveBinaryConflicts = $true ResolveBinaryConflictsName = 'PSPublishModule' NETProjectName = 'PSPublishModule' NETConfiguration = 'Release' NETFramework = 'net8.0', 'net472' NETHandleAssemblyWithSameName = $true + #NETDocumentation = $true DotSourceLibraries = $true DotSourceClasses = $true @@ -136,22 +135,54 @@ Build-Module -ModuleName 'PSPublishModule' { New-ConfigurationBuild @newConfigurationBuildSplat - New-ConfigurationArtefact -Type Unpacked -Enable -Path ([io.path]::Combine($PSScriptRoot, '..', 'Artefacts', 'Unpacked', '')) -RequiredModulesPath ([io.path]::Combine($PSScriptRoot, '..', 'Artefacts', 'Unpacked', '', 'Modules')) -AddRequiredModules -CopyFiles @{ - "Examples/Step01.CreateModuleProject.ps1" = "Examples/Step01.CreateModuleProject.ps1" - "Examples/Step02.BuildModuleOver.ps1" = "Examples/Step02.BuildModuleOver.ps1" + New-ConfigurationArtefact -Type Unpacked -Enable -Path "$PSScriptRoot\..\Artefacts\Unpacked\" -RequiredModulesPath "$PSScriptRoot\..\Artefacts\Unpacked\\Modules" -AddRequiredModules -CopyFiles @{ + "Examples\Step01.CreateModuleProject.ps1" = "Examples\Step01.CreateModuleProject.ps1" + "Examples\Step02.BuildModuleOver.ps1" = "Examples\Step02.BuildModuleOver.ps1" } -CopyFilesRelative - New-ConfigurationArtefact -Type Packed -Enable -Path ([io.path]::Combine($PSScriptRoot, '..', 'Artefacts', 'PackedWithModules')) -IncludeTagName -ID 'ToGitHub' -AddRequiredModules -CopyFiles @{ - "Examples/Step01.CreateModuleProject.ps1" = "Examples/Step01.CreateModuleProject.ps1" - "Examples/Step02.BuildModuleOver.ps1" = "Examples/Step02.BuildModuleOver.ps1" + New-ConfigurationArtefact -Type Packed -Enable -Path "$PSScriptRoot\..\Artefacts\PackedWithModules" -IncludeTagName -ID 'ToGitHub' -AddRequiredModules -CopyFiles @{ + "Examples\Step01.CreateModuleProject.ps1" = "Examples\Step01.CreateModuleProject.ps1" + "Examples\Step02.BuildModuleOver.ps1" = "Examples\Step02.BuildModuleOver.ps1" } -CopyFilesRelative -ArtefactName "PSPublishModule.-FullPackage.zip" - New-ConfigurationArtefact -Type Packed -Enable -Path ([io.path]::Combine($PSScriptRoot, '..', 'Artefacts', 'Packed')) -IncludeTagName -ID 'ToGitHub' -ArtefactName "PSPublishModule..zip" + New-ConfigurationArtefact -Type Packed -Enable -Path "$PSScriptRoot\..\Artefacts\Packed" -IncludeTagName -ID 'ToGitHub' -ArtefactName "PSPublishModule..zip" - New-ConfigurationTest -TestsPath ([io.path]::Combine($PSScriptRoot, '..', 'Tests')) -Enable + New-ConfigurationTest -TestsPath "$PSScriptRoot\..\Tests" -Enable # global options for publishing to github/psgallery # you can use FilePath where APIKey are saved in clear text or use APIKey directly #New-ConfigurationPublish -Type PowerShellGallery -FilePath 'C:\Support\Important\PowerShellGalleryAPI.txt' -Enabled:$true #New-ConfigurationPublish -Type GitHub -FilePath 'C:\Support\Important\GitHubAPI.txt' -UserName 'EvotecIT' -Enabled:$true -ID 'ToGitHub' -OverwriteTagName '' + + + ### FOR TESTING PURPOSES ONLY ### + ### SHOWING HOW THINGS WORK HERE ### + + #New-ConfigurationArtefact -Type Packed -Enable -Path "$PSScriptRoot\..\Artefacts\Packed2" -IncludeTagName -ID 'Packed2' + #New-ConfigurationArtefact -Type Packed -Enable -Path "$PSScriptRoot\..\Artefacts\Packed1" -IncludeTagName + + # those 2 are only useful for testing purposes + # New-ConfigurationArtefact -Type Script -Enable -Path "$PSScriptRoot\..\Artefacts\Script" -IncludeTagName { + # # Lets test this, this will be added in the bottom of the script + # Invoke-ModuleBuilder + # } -ID 'ToGitHubAsScript' + # New-ConfigurationArtefact -Type ScriptPacked -Enable -Path "$PSScriptRoot\..\Artefacts\ScriptPacked" -ArtefactName "Script--$((Get-Date).ToString('yyyy-MM-dd')).zip" { + # Invoke-ModuleBuilder + # } -PreScriptMerge { + # # Lets test this + # param ( + # [int]$Mode + # ) + # } -ScriptName 'Invoke-ModuleBuilder.ps1' + # New-ConfigurationArtefact -Type Script -Enable -Path "$PSScriptRoot\..\Artefacts\Script" { + # Invoke-ModuleBuilder + # } -PreScriptMerge { + # # Lets test this + # param ( + # [int]$Mode + # ) + # } -ScriptName 'Invoke-ModuleBuilder.ps1' + + #New-ConfigurationPublish -Type GitHub -FilePath 'C:\Support\Important\GitHubAPI.txt' -UserName 'EvotecIT' -Enabled:$true -ID 'ToGitHubWithoutModules' -OverwriteTagName 'v1.8.0-Preview1' + #New-ConfigurationPublish -Type GitHub -FilePath 'C:\Support\Important\GitHubAPI.txt' -UserName 'EvotecIT' -Enabled:$true -ID 'ToGitHubAsScript' } -ExitCode \ No newline at end of file diff --git a/Build/Build-ModuleSimplified.ps1 b/Build/Build-ModuleSimplified.ps1 deleted file mode 100644 index fa0d3f15..00000000 --- a/Build/Build-ModuleSimplified.ps1 +++ /dev/null @@ -1,190 +0,0 @@ -Clear-Host - -# please notice I may be using PSM1 here (not always), as the module may not be built or PSD1 may be broken -# since PSD1 is not required for proper rebuilding, we use PSM1 for this module only -# most modules should be run via PSD1 or by it's name (which in the background uses PD1) - -# This version is for local building -# We need to rmeove library before we start, as it may contain old files, which will be in use once PSD1 loads -# This is only required for PSPublisModule, as it's the only module that is being built by itself -Remove-Item -Path "C:\Support\GitHub\PSPublishModule\Lib" -Recurse -Force -ErrorAction SilentlyContinue - -Import-Module "$PSScriptRoot\..\PSPublishModule.psd1" -Force - -Build-Module -ModuleName 'PSPublishModule' { - # Usual defaults as per standard module - $Manifest = [ordered] @{ - ModuleVersion = '2.0.X' - #PreReleaseTag = 'Preview5' - CompatiblePSEditions = @('Desktop', 'Core') - GUID = 'eb76426a-1992-40a5-82cd-6480f883ef4d' - Author = 'Przemyslaw Klys' - CompanyName = 'Evotec' - Copyright = "(c) 2011 - $((Get-Date).Year) Przemyslaw Klys @ Evotec. All rights reserved." - Description = 'Simple project allowing preparing, managing, building and publishing modules to PowerShellGallery' - PowerShellVersion = '5.1' - Tags = @('Windows', 'MacOS', 'Linux', 'Build', 'Module') - IconUri = 'https://evotec.xyz/wp-content/uploads/2019/02/PSPublishModule.png' - ProjectUri = 'https://github.com/EvotecIT/PSPublishModule' - DotNetFrameworkVersion = '4.5.2' - } - New-ConfigurationManifest @Manifest - - # Add standard module dependencies (directly, but can be used with loop as well) - # New-ConfigurationModule -Type RequiredModule -Name 'platyPS', 'HelpOut' -Guid 'Auto' -Version 'Latest' - New-ConfigurationModule -Type RequiredModule -Name 'powershellget' -Guid 'Auto' -Version 'Latest' - New-ConfigurationModule -Type RequiredModule -Name 'PSScriptAnalyzer' -Guid 'Auto' -Version 'Latest' - New-ConfigurationModule -Type RequiredModule -Name 'Pester' -Version Auto -Guid Auto - - # Add external module dependencies, using loop for simplicity - New-ConfigurationModule -Type ExternalModule -Name @( - 'Microsoft.PowerShell.Utility', 'Microsoft.PowerShell.Archive', 'Microsoft.PowerShell.Management', 'Microsoft.PowerShell.Security' - ) - - # Add approved modules, that can be used as a dependency, but only when specific function from those modules is used - # And on that time only that function and dependant functions will be copied over - # Keep in mind it has it's limits when "copying" functions such as it should not depend on DLLs or other external files - New-ConfigurationModule -Type ApprovedModule -Name 'PSSharedGoods', 'PSWriteColor', 'Connectimo', 'PSUnifi', 'PSWebToolbox', 'PSMyPassword' - - New-ConfigurationModuleSkip -IgnoreModuleName 'PKI', 'OpenAuthenticode', 'platyPS', 'HelpOut' -IgnoreFunctionName @( - # ignore functions from OpenAuthenticode module when used during linux/macos build - 'Set-OpenAuthenticodeSignature' - 'Get-OpenAuthenticodeSignature' - # ignore functions from Microsoft.PowerShell.Security, as those are not on linux/macos - 'Get-AuthenticodeSignature' - 'Set-AuthenticodeSignature' - # ignore functions from PKI module when used during linux/macos build - #'Import-PfxCertificate' - 'Save-MarkdownHelp' - 'New-MarkdownHelp' - 'Update-MarkdownHelpModule' - # seems to be windows only - 'New-FileCatalog' - ) - - $ConfigurationFormat = [ordered] @{ - RemoveComments = $true - RemoveEmptyLines = $true - - PlaceOpenBraceEnable = $true - PlaceOpenBraceOnSameLine = $true - PlaceOpenBraceNewLineAfter = $true - PlaceOpenBraceIgnoreOneLineBlock = $false - - PlaceCloseBraceEnable = $true - PlaceCloseBraceNewLineAfter = $true - PlaceCloseBraceIgnoreOneLineBlock = $false - PlaceCloseBraceNoEmptyLineBefore = $true - - UseConsistentIndentationEnable = $true - UseConsistentIndentationKind = 'space' - UseConsistentIndentationPipelineIndentation = 'IncreaseIndentationAfterEveryPipeline' - UseConsistentIndentationIndentationSize = 4 - - UseConsistentWhitespaceEnable = $true - UseConsistentWhitespaceCheckInnerBrace = $true - UseConsistentWhitespaceCheckOpenBrace = $true - UseConsistentWhitespaceCheckOpenParen = $true - UseConsistentWhitespaceCheckOperator = $true - UseConsistentWhitespaceCheckPipe = $true - UseConsistentWhitespaceCheckSeparator = $true - - AlignAssignmentStatementEnable = $true - AlignAssignmentStatementCheckHashtable = $true - - UseCorrectCasingEnable = $true - } - # format PSD1 and PSM1 files when merging into a single file - # enable formatting is not required as Configuration is provided - New-ConfigurationFormat -ApplyTo 'OnMergePSM1', 'OnMergePSD1' -Sort None @ConfigurationFormat - # format PSD1 and PSM1 files within the module - # enable formatting is required to make sure that formatting is applied (with default settings) - New-ConfigurationFormat -ApplyTo 'DefaultPSD1', 'DefaultPSM1' -EnableFormatting -Sort None - # when creating PSD1 use special style without comments and with only required parameters - New-ConfigurationFormat -ApplyTo 'DefaultPSD1', 'OnMergePSD1' -PSD1Style 'Minimal' - - # configuration for documentation, at the same time it enables documentation processing - New-ConfigurationDocumentation -Enable:$true -StartClean -UpdateWhenNew -PathReadme 'Docs\Readme.md' -Path 'Docs' - - New-ConfigurationImportModule -ImportSelf - - $newConfigurationBuildSplat = @{ - Enable = $true - SignModule = $true - DeleteTargetModuleBeforeBuild = $true - MergeModuleOnBuild = $true - CertificateThumbprint = '483292C9E317AA13B07BB7A96AE9D1A5ED9E7703' - #CertificatePFXBase64 = $BasePfx - #CertificatePFXPassword = "zGT" - DoNotAttemptToFixRelativePaths = $false - SkipBuiltinReplacements = $true - - # required for Cmdlet/Alias functionality - NETProjectPath = "$PSScriptRoot\..\Sources\PSPublishModule" - ResolveBinaryConflicts = $true - ResolveBinaryConflictsName = 'PSPublishModule' - NETProjectName = 'PSPublishModule' - NETConfiguration = 'Release' - NETFramework = 'net8.0', 'net472' - NETHandleAssemblyWithSameName = $true - #NETDocumentation = $true - DotSourceLibraries = $true - DotSourceClasses = $true - - # This has to be disabled as it will not have DLLs required to do this - NETBinaryModuleCmdletScanDisabled = $true - } - - New-ConfigurationBuild @newConfigurationBuildSplat - - New-ConfigurationArtefact -Type Unpacked -Enable -Path "$PSScriptRoot\..\Artefacts\Unpacked\" -RequiredModulesPath "$PSScriptRoot\..\Artefacts\Unpacked\\Modules" -AddRequiredModules -CopyFiles @{ - "Examples\Step01.CreateModuleProject.ps1" = "Examples\Step01.CreateModuleProject.ps1" - "Examples\Step02.BuildModuleOver.ps1" = "Examples\Step02.BuildModuleOver.ps1" - } -CopyFilesRelative - - New-ConfigurationArtefact -Type Packed -Enable -Path "$PSScriptRoot\..\Artefacts\PackedWithModules" -IncludeTagName -ID 'ToGitHub' -AddRequiredModules -CopyFiles @{ - "Examples\Step01.CreateModuleProject.ps1" = "Examples\Step01.CreateModuleProject.ps1" - "Examples\Step02.BuildModuleOver.ps1" = "Examples\Step02.BuildModuleOver.ps1" - } -CopyFilesRelative -ArtefactName "PSPublishModule.-FullPackage.zip" - - New-ConfigurationArtefact -Type Packed -Enable -Path "$PSScriptRoot\..\Artefacts\Packed" -IncludeTagName -ID 'ToGitHub' -ArtefactName "PSPublishModule..zip" - - New-ConfigurationTest -TestsPath "$PSScriptRoot\..\Tests" -Enable - - # global options for publishing to github/psgallery - # you can use FilePath where APIKey are saved in clear text or use APIKey directly - #New-ConfigurationPublish -Type PowerShellGallery -FilePath 'C:\Support\Important\PowerShellGalleryAPI.txt' -Enabled:$true - #New-ConfigurationPublish -Type GitHub -FilePath 'C:\Support\Important\GitHubAPI.txt' -UserName 'EvotecIT' -Enabled:$true -ID 'ToGitHub' -OverwriteTagName '' - - - ### FOR TESTING PURPOSES ONLY ### - ### SHOWING HOW THINGS WORK HERE ### - - #New-ConfigurationArtefact -Type Packed -Enable -Path "$PSScriptRoot\..\Artefacts\Packed2" -IncludeTagName -ID 'Packed2' - #New-ConfigurationArtefact -Type Packed -Enable -Path "$PSScriptRoot\..\Artefacts\Packed1" -IncludeTagName - - # those 2 are only useful for testing purposes - # New-ConfigurationArtefact -Type Script -Enable -Path "$PSScriptRoot\..\Artefacts\Script" -IncludeTagName { - # # Lets test this, this will be added in the bottom of the script - # Invoke-ModuleBuilder - # } -ID 'ToGitHubAsScript' - # New-ConfigurationArtefact -Type ScriptPacked -Enable -Path "$PSScriptRoot\..\Artefacts\ScriptPacked" -ArtefactName "Script--$((Get-Date).ToString('yyyy-MM-dd')).zip" { - # Invoke-ModuleBuilder - # } -PreScriptMerge { - # # Lets test this - # param ( - # [int]$Mode - # ) - # } -ScriptName 'Invoke-ModuleBuilder.ps1' - # New-ConfigurationArtefact -Type Script -Enable -Path "$PSScriptRoot\..\Artefacts\Script" { - # Invoke-ModuleBuilder - # } -PreScriptMerge { - # # Lets test this - # param ( - # [int]$Mode - # ) - # } -ScriptName 'Invoke-ModuleBuilder.ps1' - - #New-ConfigurationPublish -Type GitHub -FilePath 'C:\Support\Important\GitHubAPI.txt' -UserName 'EvotecIT' -Enabled:$true -ID 'ToGitHubWithoutModules' -OverwriteTagName 'v1.8.0-Preview1' - #New-ConfigurationPublish -Type GitHub -FilePath 'C:\Support\Important\GitHubAPI.txt' -UserName 'EvotecIT' -Enabled:$true -ID 'ToGitHubAsScript' -} -ExitCode \ No newline at end of file diff --git a/Docs/Convert-ProjectEncoding.md b/Docs/Convert-ProjectEncoding.md new file mode 100644 index 00000000..3d33b9d1 --- /dev/null +++ b/Docs/Convert-ProjectEncoding.md @@ -0,0 +1,303 @@ +--- +external help file: PSPublishModule-help.xml +Module Name: PSPublishModule +online version: +schema: 2.0.0 +--- + +# Convert-ProjectEncoding + +## SYNOPSIS +Converts encoding for all source files in a project directory with comprehensive safety features. + +## SYNTAX + +### ProjectType (Default) +``` +Convert-ProjectEncoding -Path [-ProjectType ] [-SourceEncoding ] + [-TargetEncoding ] [-ExcludeDirectories ] [-CreateBackups] [-BackupDirectory ] + [-Force] [-NoRollbackOnMismatch] [-PassThru] [-ProgressAction ] [-WhatIf] [-Confirm] + [] +``` + +### Custom +``` +Convert-ProjectEncoding -Path -CustomExtensions [-SourceEncoding ] + [-TargetEncoding ] [-ExcludeDirectories ] [-CreateBackups] [-BackupDirectory ] + [-Force] [-NoRollbackOnMismatch] [-PassThru] [-ProgressAction ] [-WhatIf] [-Confirm] + [] +``` + +## DESCRIPTION +Recursively converts encoding for PowerShell, C#, and other source code files in a project directory. +Includes comprehensive safety features: WhatIf support, automatic backups, rollback protection, +and detailed reporting. +Designed specifically for development projects with intelligent file type detection. + +## EXAMPLES + +### EXAMPLE 1 +``` +Convert-ProjectEncoding -Path 'C:\MyProject' -ProjectType PowerShell -WhatIf +Preview encoding conversion for a PowerShell project (will convert from ANY encoding to UTF8BOM by default). +``` + +### EXAMPLE 2 +``` +Convert-ProjectEncoding -Path 'C:\MyProject' -ProjectType PowerShell -TargetEncoding UTF8BOM +Convert ALL files in a PowerShell project to UTF8BOM regardless of their current encoding. +``` + +### EXAMPLE 3 +``` +Convert-ProjectEncoding -Path 'C:\MyProject' -ProjectType Mixed -SourceEncoding ASCII -TargetEncoding UTF8BOM -CreateBackups +Convert ONLY ASCII files in a mixed project to UTF8BOM with backups. +``` + +### EXAMPLE 4 +``` +Convert-ProjectEncoding -Path 'C:\MyProject' -ProjectType CSharp -TargetEncoding UTF8 -PassThru +Convert ALL files in a C# project to UTF8 without BOM and return detailed results. +``` + +## PARAMETERS + +### -Path +Path to the project directory to process. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ProjectType +Type of project to process. +Determines which file extensions are included. +Valid values: 'PowerShell', 'CSharp', 'Mixed', 'All', 'Custom' + +```yaml +Type: String +Parameter Sets: ProjectType +Aliases: + +Required: False +Position: Named +Default value: Mixed +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -CustomExtensions +Custom file extensions to process when ProjectType is 'Custom'. +Example: @('*.ps1', '*.psm1', '*.cs', '*.vb') + +```yaml +Type: String[] +Parameter Sets: Custom +Aliases: + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -SourceEncoding +Expected source encoding of files. +When specified, only files with this encoding will be converted. +When not specified (or set to 'Any'), files with any encoding except the target encoding will be converted. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: Any +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -TargetEncoding +Target encoding for conversion. +Default is 'UTF8BOM' for PowerShell projects (PS 5.1 compatibility), 'UTF8' for others. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ExcludeDirectories +Directory names to exclude from processing (e.g., '.git', 'bin', 'obj'). + +```yaml +Type: String[] +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: @('.git', '.vs', 'bin', 'obj', 'packages', 'node_modules', '.vscode') +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -CreateBackups +Create backup files before conversion for additional safety. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -BackupDirectory +Directory to store backup files. +If not specified, backups are created alongside original files. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Force +Convert files even when their detected encoding doesn't match SourceEncoding. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -NoRollbackOnMismatch +Skip rolling back changes when content verification fails. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -PassThru +Return detailed results for each processed file. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -WhatIf +Shows what would happen if the cmdlet runs. +The cmdlet is not run. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: wi + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Confirm +Prompts you for confirmation before running the cmdlet. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: cf + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ProgressAction +{{ Fill ProgressAction Description }} + +```yaml +Type: ActionPreference +Parameter Sets: (All) +Aliases: proga + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +## NOTES +File type mappings: +- PowerShell: *.ps1, *.psm1, *.psd1, *.ps1xml +- CSharp: *.cs, *.csx, *.csproj, *.sln, *.config, *.json, *.xml +- Mixed: Combination of PowerShell and CSharp +- All: Common source code extensions including JS, Python, etc. + +PowerShell Encoding Recommendations: +- UTF8BOM is recommended for PowerShell files to ensure PS 5.1 compatibility +- UTF8 without BOM can cause PS 5.1 to misinterpret files as ASCII +- This can lead to broken special characters and module loading issues +- UTF8BOM ensures proper encoding detection across all PowerShell versions + +## RELATED LINKS diff --git a/Docs/Convert-ProjectLineEnding.md b/Docs/Convert-ProjectLineEnding.md new file mode 100644 index 00000000..cb23cc67 --- /dev/null +++ b/Docs/Convert-ProjectLineEnding.md @@ -0,0 +1,295 @@ +--- +external help file: PSPublishModule-help.xml +Module Name: PSPublishModule +online version: +schema: 2.0.0 +--- + +# Convert-ProjectLineEnding + +## SYNOPSIS +Converts line endings for all source files in a project directory with comprehensive safety features. + +## SYNTAX + +``` +Convert-ProjectLineEnding [-Path] [[-ProjectType] ] [[-CustomExtensions] ] + [-TargetLineEnding] [[-ExcludeDirectories] ] [-CreateBackups] [[-BackupDirectory] ] + [-Force] [-OnlyMixed] [-EnsureFinalNewline] [-OnlyMissingNewline] [-PassThru] + [-ProgressAction ] [-WhatIf] [-Confirm] [] +``` + +## DESCRIPTION +Recursively converts line endings for PowerShell, C#, and other source code files in a project directory. +Includes comprehensive safety features: WhatIf support, automatic backups, rollback protection, +and detailed reporting. +Can convert between CRLF (Windows), LF (Unix/Linux), and fix mixed line endings. + +## EXAMPLES + +### EXAMPLE 1 +``` +Convert-ProjectLineEnding -Path 'C:\MyProject' -ProjectType PowerShell -TargetLineEnding CRLF -WhatIf +Preview what files would be converted to Windows-style line endings. +``` + +### EXAMPLE 2 +``` +Convert-ProjectLineEnding -Path 'C:\MyProject' -ProjectType Mixed -TargetLineEnding LF -CreateBackups +Convert a mixed project to Unix-style line endings with backups. +``` + +### EXAMPLE 3 +``` +Convert-ProjectLineEnding -Path 'C:\MyProject' -ProjectType All -OnlyMixed -PassThru +Fix only files with mixed line endings and return detailed results. +``` + +## PARAMETERS + +### -Path +Path to the project directory to process. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: True +Position: 1 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ProjectType +Type of project to process. +Determines which file extensions are included. +Valid values: 'PowerShell', 'CSharp', 'Mixed', 'All', 'Custom' + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: 2 +Default value: Mixed +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -CustomExtensions +Custom file extensions to process when ProjectType is 'Custom'. +Example: @('*.ps1', '*.psm1', '*.cs', '*.vb') + +```yaml +Type: String[] +Parameter Sets: (All) +Aliases: + +Required: False +Position: 3 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -TargetLineEnding +Target line ending style. +Valid values: 'CRLF', 'LF' + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: True +Position: 4 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ExcludeDirectories +Directory names to exclude from processing (e.g., '.git', 'bin', 'obj'). + +```yaml +Type: String[] +Parameter Sets: (All) +Aliases: + +Required: False +Position: 5 +Default value: @('.git', '.vs', 'bin', 'obj', 'packages', 'node_modules', '.vscode') +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -CreateBackups +Create backup files before conversion for additional safety. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -BackupDirectory +Directory to store backup files. +If not specified, backups are created alongside original files. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: 6 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Force +Convert all files regardless of current line ending type. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -OnlyMixed +{{ Fill OnlyMixed Description }} + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -EnsureFinalNewline +Ensure all files end with a newline character (POSIX compliance). + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -OnlyMissingNewline +Only process files that are missing final newlines, leave others unchanged. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -PassThru +Return detailed results for each processed file. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -WhatIf +Shows what would happen if the cmdlet runs. +The cmdlet is not run. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: wi + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Confirm +Prompts you for confirmation before running the cmdlet. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: cf + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ProgressAction +{{ Fill ProgressAction Description }} + +```yaml +Type: ActionPreference +Parameter Sets: (All) +Aliases: proga + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +## NOTES +This function modifies files in place. +Always use -WhatIf first or -CreateBackups for safety. +Line ending types: +- CRLF: Windows style (\\\\r\\\\n) +- LF: Unix/Linux style (\\\\n) + +## RELATED LINKS diff --git a/Docs/Get-ProjectConsistency.md b/Docs/Get-ProjectConsistency.md new file mode 100644 index 00000000..8c713e4e --- /dev/null +++ b/Docs/Get-ProjectConsistency.md @@ -0,0 +1,217 @@ +--- +external help file: PSPublishModule-help.xml +Module Name: PSPublishModule +online version: +schema: 2.0.0 +--- + +# Get-ProjectConsistency + +## SYNOPSIS +Provides comprehensive analysis of encoding and line ending consistency across a project. + +## SYNTAX + +``` +Get-ProjectConsistency [-Path] [[-ProjectType] ] [[-CustomExtensions] ] + [[-ExcludeDirectories] ] [[-RecommendedEncoding] ] [[-RecommendedLineEnding] ] + [-ShowDetails] [[-ExportPath] ] [-ProgressAction ] [] +``` + +## DESCRIPTION +Combines encoding and line ending analysis to provide a complete picture of file consistency +across a project. +Identifies issues and provides recommendations for standardization. +This is the main analysis function that should be run before any bulk conversions. + +## EXAMPLES + +### EXAMPLE 1 +``` +Get-ProjectConsistencyReport -Path 'C:\MyProject' -ProjectType PowerShell +Analyze consistency in a PowerShell project with UTF8BOM encoding (PS 5.1 compatible). +``` + +### EXAMPLE 2 +``` +Get-ProjectConsistencyReport -Path 'C:\MyProject' -ProjectType Mixed -RecommendedEncoding UTF8BOM -RecommendedLineEnding LF -ShowDetails +Analyze a mixed project with specific recommendations and detailed output. +``` + +### EXAMPLE 3 +``` +Get-ProjectConsistencyReport -Path 'C:\MyProject' -ProjectType CSharp -RecommendedEncoding UTF8 -ExportPath 'C:\Reports\consistency-report.csv' +Analyze a C# project (UTF8 without BOM is fine) with CSV export. +``` + +## PARAMETERS + +### -Path +Path to the project directory to analyze. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: True +Position: 1 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ProjectType +Type of project to analyze. +Determines which file extensions are included. +Valid values: 'PowerShell', 'CSharp', 'Mixed', 'All', 'Custom' + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: 2 +Default value: Mixed +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -CustomExtensions +Custom file extensions to analyze when ProjectType is 'Custom'. +Example: @('*.ps1', '*.psm1', '*.cs', '*.vb') + +```yaml +Type: String[] +Parameter Sets: (All) +Aliases: + +Required: False +Position: 3 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ExcludeDirectories +Directory names to exclude from analysis (e.g., '.git', 'bin', 'obj'). + +```yaml +Type: String[] +Parameter Sets: (All) +Aliases: + +Required: False +Position: 4 +Default value: @('.git', '.vs', 'bin', 'obj', 'packages', 'node_modules', '.vscode') +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -RecommendedEncoding +The encoding standard you want to achieve. +Default is 'UTF8BOM' for PowerShell projects (PS 5.1 compatibility), 'UTF8' for others. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: 5 +Default value: $( + if ($ProjectType -eq 'PowerShell') { 'UTF8BOM' } + elseif ($ProjectType -eq 'Mixed') { 'UTF8BOM' } # Default to PowerShell-safe for mixed projects + else { 'UTF8' } + ) +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -RecommendedLineEnding +The line ending standard you want to achieve. +Default is 'CRLF' on Windows, 'LF' on Unix. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: 6 +Default value: $(if ($IsWindows) { 'CRLF' } else { 'LF' }) +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ShowDetails +Include detailed file-by-file analysis in the output. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ExportPath +Export the detailed report to a CSV file at the specified path. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: 7 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ProgressAction +{{ Fill ProgressAction Description }} + +```yaml +Type: ActionPreference +Parameter Sets: (All) +Aliases: proga + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +## NOTES +This function combines the analysis from Get-ProjectEncoding and Get-ProjectLineEnding +to provide a unified view of project file consistency. +Use this before running conversion functions. + +Encoding Recommendations: +- PowerShell: UTF8BOM (required for PS 5.1 compatibility with special characters) +- C#: UTF8 (BOM not needed, Visual Studio handles UTF8 correctly) +- Mixed: UTF8BOM (safest for cross-platform PowerShell compatibility) + +PowerShell 5.1 Compatibility: +UTF8 without BOM can cause PowerShell 5.1 to misinterpret files as ASCII, leading to: +- Broken special characters and accented letters +- Module import failures +- Incorrect string processing +UTF8BOM ensures proper encoding detection across all PowerShell versions. + +## RELATED LINKS diff --git a/Docs/Get-ProjectEncoding.md b/Docs/Get-ProjectEncoding.md new file mode 100644 index 00000000..11031048 --- /dev/null +++ b/Docs/Get-ProjectEncoding.md @@ -0,0 +1,182 @@ +--- +external help file: PSPublishModule-help.xml +Module Name: PSPublishModule +online version: +schema: 2.0.0 +--- + +# Get-ProjectEncoding + +## SYNOPSIS +Analyzes encoding consistency across all files in a project directory. + +## SYNTAX + +``` +Get-ProjectEncoding [-Path] [[-ProjectType] ] [[-CustomExtensions] ] + [[-ExcludeDirectories] ] [-GroupByEncoding] [-ShowFiles] [[-ExportPath] ] + [-ProgressAction ] [] +``` + +## DESCRIPTION +Scans all relevant files in a project directory and provides a comprehensive report on file encodings. +Identifies inconsistencies, potential issues, and provides recommendations for standardization. +Useful for auditing projects before performing encoding conversions. + +## EXAMPLES + +### EXAMPLE 1 +``` +Get-ProjectEncoding -Path 'C:\MyProject' -ProjectType PowerShell +Analyze encoding consistency in a PowerShell project. +``` + +### EXAMPLE 2 +``` +Get-ProjectEncoding -Path 'C:\MyProject' -ProjectType Mixed -GroupByEncoding -ShowFiles +Get detailed encoding report grouped by encoding type with individual file listings. +``` + +### EXAMPLE 3 +``` +Get-ProjectEncoding -Path 'C:\MyProject' -ProjectType All -ExportPath 'C:\Reports\encoding-report.csv' +Analyze all file types and export detailed report to CSV. +``` + +## PARAMETERS + +### -Path +Path to the project directory to analyze. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: True +Position: 1 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ProjectType +Type of project to analyze. +Determines which file extensions are included. +Valid values: 'PowerShell', 'CSharp', 'Mixed', 'All', 'Custom' + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: 2 +Default value: Mixed +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -CustomExtensions +Custom file extensions to analyze when ProjectType is 'Custom'. +Example: @('*.ps1', '*.psm1', '*.cs', '*.vb') + +```yaml +Type: String[] +Parameter Sets: (All) +Aliases: + +Required: False +Position: 3 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ExcludeDirectories +Directory names to exclude from analysis (e.g., '.git', 'bin', 'obj'). + +```yaml +Type: String[] +Parameter Sets: (All) +Aliases: + +Required: False +Position: 4 +Default value: @('.git', '.vs', 'bin', 'obj', 'packages', 'node_modules', '.vscode') +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -GroupByEncoding +Group results by encoding type for easier analysis. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ShowFiles +Include individual file details in the report. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ExportPath +Export the detailed report to a CSV file at the specified path. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: 5 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ProgressAction +{{ Fill ProgressAction Description }} + +```yaml +Type: ActionPreference +Parameter Sets: (All) +Aliases: proga + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +## NOTES +This function is read-only and does not modify any files. +Use Convert-ProjectEncoding to standardize encodings. + +## RELATED LINKS diff --git a/Docs/Get-ProjectLineEnding.md b/Docs/Get-ProjectLineEnding.md new file mode 100644 index 00000000..03a8fefe --- /dev/null +++ b/Docs/Get-ProjectLineEnding.md @@ -0,0 +1,201 @@ +--- +external help file: PSPublishModule-help.xml +Module Name: PSPublishModule +online version: +schema: 2.0.0 +--- + +# Get-ProjectLineEnding + +## SYNOPSIS +Analyzes line ending consistency across all files in a project directory. + +## SYNTAX + +``` +Get-ProjectLineEnding [-Path] [[-ProjectType] ] [[-CustomExtensions] ] + [[-ExcludeDirectories] ] [-GroupByLineEnding] [-ShowFiles] [-CheckMixed] [[-ExportPath] ] + [-ProgressAction ] [] +``` + +## DESCRIPTION +Scans all relevant files in a project directory and provides a comprehensive report on line endings. +Identifies inconsistencies between CRLF (Windows), LF (Unix/Linux), and mixed line endings. +Helps ensure consistency across development environments and prevent Git issues. + +## EXAMPLES + +### EXAMPLE 1 +``` +Get-ProjectLineEnding -Path 'C:\MyProject' -ProjectType PowerShell +Analyze line ending consistency in a PowerShell project. +``` + +### EXAMPLE 2 +``` +Get-ProjectLineEnding -Path 'C:\MyProject' -ProjectType Mixed -CheckMixed -ShowFiles +Get detailed line ending report including mixed line ending detection. +``` + +### EXAMPLE 3 +``` +Get-ProjectLineEnding -Path 'C:\MyProject' -ProjectType All -ExportPath 'C:\Reports\lineending-report.csv' +Analyze all file types and export detailed report to CSV. +``` + +## PARAMETERS + +### -Path +Path to the project directory to analyze. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: True +Position: 1 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ProjectType +Type of project to analyze. +Determines which file extensions are included. +Valid values: 'PowerShell', 'CSharp', 'Mixed', 'All', 'Custom' + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: 2 +Default value: Mixed +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -CustomExtensions +Custom file extensions to analyze when ProjectType is 'Custom'. +Example: @('*.ps1', '*.psm1', '*.cs', '*.vb') + +```yaml +Type: String[] +Parameter Sets: (All) +Aliases: + +Required: False +Position: 3 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ExcludeDirectories +Directory names to exclude from analysis (e.g., '.git', 'bin', 'obj'). + +```yaml +Type: String[] +Parameter Sets: (All) +Aliases: + +Required: False +Position: 4 +Default value: @('.git', '.vs', 'bin', 'obj', 'packages', 'node_modules', '.vscode') +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -GroupByLineEnding +Group results by line ending type for easier analysis. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ShowFiles +Include individual file details in the report. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -CheckMixed +Additionally check for files with mixed line endings (both CRLF and LF in same file). + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ExportPath +Export the detailed report to a CSV file at the specified path. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: 5 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ProgressAction +{{ Fill ProgressAction Description }} + +```yaml +Type: ActionPreference +Parameter Sets: (All) +Aliases: proga + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +## NOTES +Line ending types: +- CRLF: Windows style (\\\\r\\\\n) +- LF: Unix/Linux style (\\\\n) +- CR: Classic Mac style (\\\\r) - rarely used +- Mixed: File contains multiple line ending types +- None: Empty file or single line without line ending + +## RELATED LINKS diff --git a/Docs/Readme.md b/Docs/Readme.md index d2f52d3f..4e25c377 100644 --- a/Docs/Readme.md +++ b/Docs/Readme.md @@ -14,12 +14,27 @@ Locale: en-US ### [Convert-CommandsToList](Convert-CommandsToList.md) {{ Fill in the Description }} +### [Convert-ProjectEncoding](Convert-ProjectEncoding.md) +{{ Fill in the Description }} + +### [Convert-ProjectLineEnding](Convert-ProjectLineEnding.md) +{{ Fill in the Description }} + ### [Get-MissingFunctions](Get-MissingFunctions.md) {{ Fill in the Description }} ### [Get-PowerShellAssemblyMetadata](Get-PowerShellAssemblyMetadata.md) {{ Fill in the Description }} +### [Get-ProjectConsistency](Get-ProjectConsistency.md) +{{ Fill in the Description }} + +### [Get-ProjectEncoding](Get-ProjectEncoding.md) +{{ Fill in the Description }} + +### [Get-ProjectLineEnding](Get-ProjectLineEnding.md) +{{ Fill in the Description }} + ### [Get-ProjectVersion](Get-ProjectVersion.md) {{ Fill in the Description }} diff --git a/Examples/Example.ProjectEncodingNewLines.ps1 b/Examples/Example.ProjectEncodingNewLines.ps1 new file mode 100644 index 00000000..4d11f55f --- /dev/null +++ b/Examples/Example.ProjectEncodingNewLines.ps1 @@ -0,0 +1,23 @@ +Import-Module .\PSPublishModule.psd1 -Force + +Write-Host "=== Testing All Project Analysis and Conversion Examples ===" -ForegroundColor Magenta + +# Test encoding analysis +Write-Host "`n=== 1. Encoding Analysis ===" -ForegroundColor Cyan +Get-ProjectEncoding -Path 'C:\Support\GitHub\PSPublishModule' -ProjectType PowerShell -ExcludeDirectories 'Artefacts', 'Ignore' + +# Test line ending analysis +Write-Host "`n=== 2. Line Ending Analysis ===" -ForegroundColor Cyan +Get-ProjectLineEnding -Path 'C:\Support\GitHub\PSPublishModule' -ProjectType PowerShell -ExcludeDirectories 'Artefacts', 'Ignore' + +# Test encoding conversion (WhatIf) +Write-Host "`n=== 3. Encoding Conversion Preview ===" -ForegroundColor Cyan +Convert-ProjectEncoding -Path 'C:\Support\GitHub\PSPublishModule' -ProjectType PowerShell -TargetEncoding UTF8BOM -WhatIf:$true -ExcludeDirectories 'Artefacts', 'Ignore' + +# Test line ending conversion (WhatIf) +Write-Host "`n=== 4. Line Ending Conversion Preview ===" -ForegroundColor Cyan +Convert-ProjectLineEnding -Path 'C:\Support\GitHub\PSPublishModule' -ProjectType PowerShell -TargetLineEnding CRLF -WhatIf:$true -ExcludeDirectories 'Artefacts', 'Ignore' + +Write-Host "`n=== All Examples Completed Successfully! ===" -ForegroundColor Green +Write-Host "PowerShell Edition: $($PSVersionTable.PSEdition)" -ForegroundColor Yellow +Write-Host "PowerShell Version: $($PSVersionTable.PSVersion)" -ForegroundColor Yellow diff --git a/Examples/Example.TestEncoding.ps1 b/Examples/Example.TestEncoding.ps1 new file mode 100644 index 00000000..f0c7174d --- /dev/null +++ b/Examples/Example.TestEncoding.ps1 @@ -0,0 +1,9 @@ +Import-Module .\PSPublishModule.psd1 -Force + +# Test encoding analysis +Write-Host "=== Encoding Analysis Demo ===" -ForegroundColor Cyan +$Summary = Get-ProjectEncoding -Path 'C:\Support\GitHub\PSPublishModule' -ProjectType PowerShell -GroupByEncoding -ShowFiles -ExcludeDirectories 'Artefacts' +$Summary | Format-List +$Summary.Files | Format-Table -AutoSize RelativePath, Extension, Encoding +$Summary.GroupedByEncoding | Format-List +$Summary.Summary | Format-List \ No newline at end of file diff --git a/Examples/Example.TestEncodingConversion.ps1 b/Examples/Example.TestEncodingConversion.ps1 new file mode 100644 index 00000000..e69220ce --- /dev/null +++ b/Examples/Example.TestEncodingConversion.ps1 @@ -0,0 +1,13 @@ +Import-Module .\PSPublishModule.psd1 -Force + +# Example 1: Convert ALL files to UTF8BOM (recommended for PowerShell projects) +Write-Host "=== Convert ALL files to UTF8BOM (default behavior) ===" -ForegroundColor Cyan +Convert-ProjectEncoding -Path 'C:\Support\GitHub\PSPublishModule' -ProjectType PowerShell -TargetEncoding UTF8BOM -WhatIf:$true -ExcludeDirectories 'Artefacts' -Verbose + +# Example 2: Convert only ASCII files to UTF8BOM +Write-Host "`n=== Convert ONLY ASCII files to UTF8BOM ===" -ForegroundColor Cyan +Convert-ProjectEncoding -Path 'C:\Support\GitHub\PSPublishModule' -ProjectType PowerShell -SourceEncoding ASCII -TargetEncoding UTF8BOM -WhatIf:$true -ExcludeDirectories 'Artefacts' -Verbose + +# Example 3: Convert from any encoding to UTF8 (no BOM) for cross-platform compatibility (this is going to break PowerShell 5.1 scripts that expect UTF8 with BOM) +Write-Host "`n=== Convert ALL files to UTF8 (no BOM) ===" -ForegroundColor Cyan +Convert-ProjectEncoding -Path 'C:\Support\GitHub\PSPublishModule' -ProjectType PowerShell -TargetEncoding UTF8 -WhatIf:$true -ExcludeDirectories 'Artefacts' -Verbose \ No newline at end of file diff --git a/Examples/Example.TestLineEnding.ps1 b/Examples/Example.TestLineEnding.ps1 new file mode 100644 index 00000000..502fca43 --- /dev/null +++ b/Examples/Example.TestLineEnding.ps1 @@ -0,0 +1,16 @@ +Import-Module .\PSPublishModule.psd1 -Force + +# Test line ending analysis +Write-Host "=== Line Ending Analysis Demo ===" -ForegroundColor Cyan +Get-ProjectLineEnding -Path 'C:\Support\GitHub\PSPublishModule' -ProjectType PowerShell -GroupByLineEnding -ShowFiles -CheckMixed + +# Test PowerShell 5.1 compatibility +Write-Host "`n=== PowerShell Compatibility Test ===" -ForegroundColor Cyan +Write-Host "PowerShell Edition: $($PSVersionTable.PSEdition)" -ForegroundColor Yellow +Write-Host "PowerShell Version: $($PSVersionTable.PSVersion)" -ForegroundColor Yellow + +# Test with different project types +Write-Host "`n=== Mixed Project Analysis ===" -ForegroundColor Cyan +Get-ProjectLineEnding -Path 'C:\Support\GitHub\PSPublishModule' -ProjectType Mixed -ExcludeDirectories 'Artefacts', 'Ignore' -CheckMixed + +Write-Host "`n=== Line Ending Analysis Complete! ===" -ForegroundColor Green \ No newline at end of file diff --git a/Examples/Example.TestLineEndingConversion.ps1 b/Examples/Example.TestLineEndingConversion.ps1 new file mode 100644 index 00000000..9426b809 --- /dev/null +++ b/Examples/Example.TestLineEndingConversion.ps1 @@ -0,0 +1,17 @@ +Import-Module .\PSPublishModule.psd1 -Force + +# Example 1: Convert ALL files to CRLF (Windows line endings) - recommended for Windows development +Write-Host "=== Convert ALL files to CRLF (Windows line endings) ===" -ForegroundColor Cyan +Convert-ProjectLineEnding -Path 'C:\Support\GitHub\PSPublishModule' -ProjectType PowerShell -TargetLineEnding CRLF -WhatIf:$true -ExcludeDirectories 'Artefacts', 'Ignore' -Verbose + +# Example 2: Convert ALL files to LF (Unix/Linux line endings) - recommended for cross-platform +Write-Host "`n=== Convert ALL files to LF (Unix/Linux line endings) ===" -ForegroundColor Cyan +Convert-ProjectLineEnding -Path 'C:\Support\GitHub\PSPublishModule' -ProjectType PowerShell -TargetLineEnding LF -WhatIf:$true -ExcludeDirectories 'Artefacts', 'Ignore' -Verbose + +# Example 3: Ensure all files have final newlines (POSIX compliance) +Write-Host "`n=== Ensure all files end with newline (POSIX compliance) ===" -ForegroundColor Cyan +Convert-ProjectLineEnding -Path 'C:\Support\GitHub\PSPublishModule' -ProjectType PowerShell -TargetLineEnding LF -EnsureFinalNewline -WhatIf:$true -ExcludeDirectories 'Artefacts', 'Ignore' -Verbose + +# Example 4: Only fix files missing final newlines (keep current line ending style) +Write-Host "`n=== Fix ONLY files missing final newlines ===" -ForegroundColor Cyan +Convert-ProjectLineEnding -Path 'C:\Support\GitHub\PSPublishModule' -ProjectType PowerShell -TargetLineEnding CRLF -OnlyMissingNewline -WhatIf:$true -ExcludeDirectories 'Artefacts', 'Ignore' -Verbose \ No newline at end of file diff --git a/PSPublishModule.psd1 b/PSPublishModule.psd1 index 750a7fe7..4d505468 100644 --- a/PSPublishModule.psd1 +++ b/PSPublishModule.psd1 @@ -7,9 +7,9 @@ Copyright = '(c) 2011 - 2025 Przemyslaw Klys @ Evotec. All rights reserved.' Description = 'Simple project allowing preparing, managing, building and publishing modules to PowerShellGallery' DotNetFrameworkVersion = '4.5.2' - FunctionsToExport = @('Convert-CommandsToList', 'Get-MissingFunctions', 'Get-PowerShellAssemblyMetadata', 'Get-ProjectVersion', 'Initialize-PortableModule', 'Initialize-PortableScript', 'Initialize-ProjectManager', 'Invoke-DotNetReleaseBuild', 'Invoke-ModuleBuild', 'New-ConfigurationArtefact', 'New-ConfigurationBuild', 'New-ConfigurationCommand', 'New-ConfigurationDocumentation', 'New-ConfigurationExecute', 'New-ConfigurationFormat', 'New-ConfigurationImportModule', 'New-ConfigurationInformation', 'New-ConfigurationManifest', 'New-ConfigurationModule', 'New-ConfigurationModuleSkip', 'New-ConfigurationPlaceHolder', 'New-ConfigurationPublish', 'New-ConfigurationTest', 'Publish-GitHubReleaseAsset', 'Publish-NugetPackage', 'Register-Certificate', 'Remove-Comments', 'Send-GitHubRelease', 'Set-ProjectVersion', 'Test-BasicModule', 'Test-ScriptFile', 'Test-ScriptModule') + FunctionsToExport = @('Convert-CommandsToList', 'Convert-ProjectEncoding', 'Convert-ProjectLineEnding', 'Get-MissingFunctions', 'Get-PowerShellAssemblyMetadata', 'Get-ProjectConsistency', 'Get-ProjectEncoding', 'Get-ProjectLineEnding', 'Get-ProjectVersion', 'Initialize-PortableModule', 'Initialize-PortableScript', 'Initialize-ProjectManager', 'Invoke-DotNetReleaseBuild', 'Invoke-ModuleBuild', 'New-ConfigurationArtefact', 'New-ConfigurationBuild', 'New-ConfigurationCommand', 'New-ConfigurationDocumentation', 'New-ConfigurationExecute', 'New-ConfigurationFormat', 'New-ConfigurationImportModule', 'New-ConfigurationInformation', 'New-ConfigurationManifest', 'New-ConfigurationModule', 'New-ConfigurationModuleSkip', 'New-ConfigurationPlaceHolder', 'New-ConfigurationPublish', 'New-ConfigurationTest', 'Publish-GitHubReleaseAsset', 'Publish-NugetPackage', 'Register-Certificate', 'Remove-Comments', 'Send-GitHubRelease', 'Set-ProjectVersion', 'Test-BasicModule', 'Test-ScriptFile', 'Test-ScriptModule') GUID = 'eb76426a-1992-40a5-82cd-6480f883ef4d' - ModuleVersion = '2.0.21' + ModuleVersion = '2.0.22' PowerShellVersion = '5.1' PrivateData = @{ PSData = @{ diff --git a/Private/Convert-FileEncoding.ps1 b/Private/Convert-FileEncoding.ps1 new file mode 100644 index 00000000..1658dd44 --- /dev/null +++ b/Private/Convert-FileEncoding.ps1 @@ -0,0 +1,82 @@ +function Convert-FileEncoding { + <# + .SYNOPSIS + Converts files from one encoding to another. + + .DESCRIPTION + Reads a single file or all files within a directory and rewrites them using a new encoding. + Useful for converting files from UTF8 with BOM to UTF8 without BOM or any other supported encoding. + Files are only converted when their detected encoding matches the provided SourceEncoding unless -Force is used. + If a file already uses the target encoding it is skipped. + After conversion the content is verified to ensure it matches the original string. + If the content differs the change is rolled back by default unless -NoRollbackOnMismatch is specified. + Supports -WhatIf for previewing changes. + + .PARAMETER Path + Specifies the file or directory to process. + + .PARAMETER Filter + Filters which files are processed when Path is a directory. Wildcards are supported. + + .PARAMETER SourceEncoding + Encoding used when reading files. The default is UTF8BOM. + + .PARAMETER TargetEncoding + Encoding used when writing files. The default is UTF8. + + .PARAMETER Recurse + When Path is a directory, process files in all subdirectories as well. + + .PARAMETER Force + Convert files even when their detected encoding does not match SourceEncoding. + + .PARAMETER NoRollbackOnMismatch + Skip rolling back files when the verification step detects that content changed during conversion. + + .EXAMPLE + Convert-FileEncoding -Path 'C:\Scripts' -Filter '*.ps1' -SourceEncoding UTF8BOM -TargetEncoding UTF8 + + Converts all PowerShell scripts under C:\Scripts from UTF8 with BOM to UTF8. + #> + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory)] + [string] $Path, + + [string] $Filter = '*.*', + + [ValidateSet('Ascii','BigEndianUnicode','Unicode','UTF7','UTF8','UTF8BOM','UTF32','Default','OEM')] + [string] $SourceEncoding = 'UTF8BOM', + + [ValidateSet('Ascii','BigEndianUnicode','Unicode','UTF7','UTF8','UTF8BOM','UTF32','Default','OEM')] + [string] $TargetEncoding = 'UTF8', + + [switch] $Recurse, + [switch] $Force, + [switch] $NoRollbackOnMismatch + ) + + $source = Resolve-Encoding -Name $SourceEncoding + $target = Resolve-Encoding -Name $TargetEncoding + + if (Test-Path -LiteralPath $Path -PathType Leaf) { + $files = Get-Item -LiteralPath $Path + } elseif (Test-Path -LiteralPath $Path -PathType Container) { + $gciParams = @{ LiteralPath = $Path; File = $true; Filter = $Filter } + if ($Recurse) { $gciParams.Recurse = $true } + $files = Get-ChildItem @gciParams + } else { + throw "Path $Path not found" + } + + foreach ($file in $files) { + $result = Convert-FileEncodingSingle -FilePath $file.FullName -SourceEncoding $source -TargetEncoding $target -Force:$Force -NoRollbackOnMismatch:$NoRollbackOnMismatch -WhatIf:$WhatIfPreference + + if ($result) { + Write-Verbose "File: $($result.FilePath) - Status: $($result.Status) - Reason: $($result.Reason)" + if ($result.Warning) { + Write-Warning $result.Warning + } + } + } +} diff --git a/Private/Convert-FileEncodingSingle.ps1 b/Private/Convert-FileEncodingSingle.ps1 new file mode 100644 index 00000000..7a9b026e --- /dev/null +++ b/Private/Convert-FileEncodingSingle.ps1 @@ -0,0 +1,175 @@ +function Convert-FileEncodingSingle { + <# + .SYNOPSIS + Converts a single file from one encoding to another with validation and rollback protection. + + .DESCRIPTION + Internal helper function that converts a single file's encoding with comprehensive validation. + Includes content verification and automatic rollback on mismatch to prevent data corruption. + + .PARAMETER FilePath + Full path to the file to convert. + + .PARAMETER SourceEncoding + Expected source encoding of the file. + + .PARAMETER TargetEncoding + Target encoding to convert the file to. + + .PARAMETER Force + Convert file even if detected encoding doesn't match SourceEncoding. + + .PARAMETER NoRollbackOnMismatch + Skip rolling back changes when content verification fails. + + .PARAMETER CreateBackup + Create a backup file before conversion for additional safety. + #> + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory)] + [string] $FilePath, + + [Parameter(Mandatory)] + [System.Text.Encoding] $SourceEncoding, + + [Parameter(Mandatory)] + [System.Text.Encoding] $TargetEncoding, + + [switch] $Force, + [switch] $NoRollbackOnMismatch, + [switch] $CreateBackup + ) + + $bytesBefore = $null + $backupPath = $null + + try { + # Get current file encoding + $detectedObj = Get-FileEncoding -Path $FilePath -AsObject + $detected = $detectedObj.Encoding + $detectedName = $detectedObj.EncodingName + + # More robust encoding comparison using encoding names from our detection + $sourceExpected = if ($SourceEncoding -is [System.Text.UTF8Encoding] -and $SourceEncoding.GetPreamble().Length -eq 3) { 'UTF8BOM' } + elseif ($SourceEncoding -is [System.Text.UTF8Encoding]) { 'UTF8' } + elseif ($SourceEncoding -is [System.Text.UnicodeEncoding]) { 'Unicode' } + elseif ($SourceEncoding -is [System.Text.UTF7Encoding]) { 'UTF7' } + elseif ($SourceEncoding -is [System.Text.UTF32Encoding]) { 'UTF32' } + elseif ($SourceEncoding -is [System.Text.ASCIIEncoding]) { 'ASCII' } + elseif ($SourceEncoding -is [System.Text.BigEndianUnicodeEncoding]) { 'BigEndianUnicode' } + else { $SourceEncoding.WebName } + + # Check if source encoding matches detected encoding + if ($detectedName -ne $sourceExpected -and -not $Force) { + Write-Verbose "Skipping $FilePath because detected encoding '$detectedName' does not match expected '$sourceExpected'." + return @{ + FilePath = $FilePath + Status = 'Skipped' + Reason = "Encoding mismatch: detected '$detectedName', expected '$sourceExpected'" + DetectedEncoding = $detectedName + } + } + + # Check if already target encoding + $targetExpected = if ($TargetEncoding -is [System.Text.UTF8Encoding] -and $TargetEncoding.GetPreamble().Length -eq 3) { 'UTF8BOM' } + elseif ($TargetEncoding -is [System.Text.UTF8Encoding]) { 'UTF8' } + elseif ($TargetEncoding -is [System.Text.UnicodeEncoding]) { 'Unicode' } + elseif ($TargetEncoding -is [System.Text.UTF7Encoding]) { 'UTF7' } + elseif ($TargetEncoding -is [System.Text.UTF32Encoding]) { 'UTF32' } + elseif ($TargetEncoding -is [System.Text.ASCIIEncoding]) { 'ASCII' } + elseif ($TargetEncoding -is [System.Text.BigEndianUnicodeEncoding]) { 'BigEndianUnicode' } + else { $TargetEncoding.WebName } + + if ($detectedName -eq $targetExpected) { + Write-Verbose "Skipping $FilePath because encoding is already '$targetExpected'." + return @{ + FilePath = $FilePath + Status = 'Skipped' + Reason = "Already target encoding '$targetExpected'" + DetectedEncoding = $detectedName + } + } + + if ($PSCmdlet.ShouldProcess($FilePath, "Convert from '$detectedName' to '$targetExpected'")) { + # Read original content and create backup + $content = [System.IO.File]::ReadAllText($FilePath, $detected) + $bytesBefore = [System.IO.File]::ReadAllBytes($FilePath) + + if ($CreateBackup) { + $backupPath = "$FilePath.backup" + $counter = 1 + while (Test-Path $backupPath) { + $backupPath = "$FilePath.backup$counter" + $counter++ + } + [System.IO.File]::WriteAllBytes($backupPath, $bytesBefore) + Write-Verbose "Created backup at: $backupPath" + } + + # Convert the file + [System.IO.File]::WriteAllText($FilePath, $content, $TargetEncoding) + + # Verify conversion + $convertedContent = [System.IO.File]::ReadAllText($FilePath, $TargetEncoding) + if ($convertedContent -ne $content) { + $warningMsg = "Content verification failed for $FilePath - characters may have been lost during conversion" + Write-Warning $warningMsg + + if (-not $NoRollbackOnMismatch) { + [System.IO.File]::WriteAllBytes($FilePath, $bytesBefore) + Write-Warning "Reverted changes to $FilePath due to content mismatch" + + return @{ + FilePath = $FilePath + Status = 'Failed' + Reason = 'Content verification failed - reverted' + DetectedEncoding = $detectedName + BackupPath = $backupPath + } + } else { + return @{ + FilePath = $FilePath + Status = 'Converted' + Reason = 'Content verification failed but conversion kept' + DetectedEncoding = $detectedName + TargetEncoding = $targetExpected + BackupPath = $backupPath + Warning = $warningMsg + } + } + } + + Write-Verbose "Successfully converted $FilePath from '$detectedName' to '$targetExpected'" + return @{ + FilePath = $FilePath + Status = 'Converted' + Reason = 'Successfully converted' + DetectedEncoding = $detectedName + TargetEncoding = $targetExpected + BackupPath = $backupPath + } + } + } catch { + $errorMsg = "Failed to convert ${FilePath}: $_" + Write-Warning $errorMsg + + # Attempt rollback on error + if (-not $NoRollbackOnMismatch -and $bytesBefore) { + try { + [System.IO.File]::WriteAllBytes($FilePath, $bytesBefore) + Write-Verbose "Rolled back $FilePath due to conversion error" + } catch { + Write-Warning "Failed to rollback $FilePath after error: $_" + } + } + + return @{ + FilePath = $FilePath + Status = 'Error' + Reason = $errorMsg + DetectedEncoding = $detectedName + BackupPath = $backupPath + } + } +} diff --git a/Private/Convert-FolderEncoding.ps1 b/Private/Convert-FolderEncoding.ps1 new file mode 100644 index 00000000..219c0c17 --- /dev/null +++ b/Private/Convert-FolderEncoding.ps1 @@ -0,0 +1,305 @@ +function Convert-FolderEncoding { + <# + .SYNOPSIS + Converts files in folders to a target encoding based on file extensions. + + .DESCRIPTION + A user-friendly wrapper around Convert-FileEncoding that makes it easy to target specific file types + by their extensions across one or more folders. This function is ideal for scenarios where you want to convert encoding for + specific file types across directories without needing to know filter syntax. + + The function supports both single and multiple file extensions, with smart defaults for PowerShell + compatibility. It provides comprehensive feedback and safety features including WhatIf support + and backup creation. + + .PARAMETER Path + The directory path to search for files. Can be a single directory or an array of directories. + Use '.' for the current directory. + + .PARAMETER Extensions + File extensions to target for conversion. Can be specified with or without the leading dot. + Examples: 'ps1', '.ps1', @('ps1', 'psm1'), @('.cs', '.vb') + + Common presets available via -FileType parameter for convenience. + + .PARAMETER FileType + Predefined file type groups for common scenarios: + - PowerShell: .ps1, .psm1, .psd1, .ps1xml + - CSharp: .cs, .csx + - Web: .html, .css, .js, .json, .xml + - Scripts: .ps1, .py, .rb, .sh, .bat, .cmd + - Text: .txt, .md, .log, .config + - All: Processes all common text file types + + .PARAMETER SourceEncoding + Expected source encoding of files. Default is 'UTF8BOM'. + + .PARAMETER TargetEncoding + Target encoding for conversion. + Default is 'UTF8BOM' for PowerShell compatibility (prevents PS 5.1 ASCII misinterpretation). + + .PARAMETER ExcludeDirectories + Directory names to exclude from processing (e.g., '.git', 'bin', 'obj', 'node_modules'). + Default excludes common build and version control directories. + + .PARAMETER Recurse + Process files in subdirectories recursively. Default is $true. + + .PARAMETER CreateBackups + Create backup files before conversion for additional safety. + Backups are created with .bak extension in the same directory. + + .PARAMETER Force + Convert files even when their detected encoding doesn't match SourceEncoding. + + .PARAMETER NoRollbackOnMismatch + Skip rolling back files when verification detects content changes during conversion. + + .PARAMETER MaxDepth + Maximum directory depth to recurse when -Recurse is enabled. Default is unlimited. + + .PARAMETER PassThru + Return conversion results for further processing. + + .EXAMPLE + Convert-FolderEncoding -Path . -Extensions 'ps1' -WhatIf + + Preview what PowerShell files in the current directory would be converted. + + .EXAMPLE + Convert-FolderEncoding -Path @('.\Scripts', '.\Modules') -FileType PowerShell -CreateBackups + + Convert all PowerShell files in Scripts and Modules directories to UTF8BOM with backups. + + .EXAMPLE + Convert-FolderEncoding -Path . -Extensions @('cs', 'vb') -TargetEncoding UTF8 -Recurse + + Convert all C# and VB.NET files recursively to UTF8 (without BOM). + + .EXAMPLE + Convert-FolderEncoding -Path .\Source -FileType Web -ExcludeDirectories @('node_modules', 'dist') -Force + + Convert web files, excluding build directories, forcing conversion regardless of detected encoding. + + .NOTES + Author: PowerShell Encoding Tools + + This function provides a more user-friendly interface than Convert-FileEncoding for common scenarios. + For complex filtering requirements, use Convert-FileEncoding directly. + + PowerShell Encoding Notes: + - UTF8BOM is recommended for PowerShell files to ensure PS 5.1 compatibility + - UTF8 without BOM can cause PS 5.1 to misinterpret files as ASCII + - Always test with -WhatIf first and consider using -CreateBackups + #> + [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Extensions')] + param( + [Parameter(Mandatory)] + [Alias('Directory', 'Folder')] + [string[]] $Path, + + [Parameter(ParameterSetName = 'Extensions', Mandatory)] + [string[]] $Extensions, + + [Parameter(ParameterSetName = 'FileType', Mandatory)] + [ValidateSet('PowerShell', 'CSharp', 'Web', 'Scripts', 'Text', 'All')] + [string] $FileType, + + [ValidateSet('Ascii', 'BigEndianUnicode', 'Unicode', 'UTF7', 'UTF8', 'UTF8BOM', 'UTF32', 'Default', 'OEM')] + [string] $SourceEncoding = 'UTF8BOM', + + [ValidateSet('Ascii', 'BigEndianUnicode', 'Unicode', 'UTF7', 'UTF8', 'UTF8BOM', 'UTF32', 'Default', 'OEM')] + [string] $TargetEncoding = 'UTF8BOM', + + [string[]] $ExcludeDirectories = @('.git', '.vs', 'bin', 'obj', 'packages', 'node_modules', '.vscode', 'dist', 'build'), + + [bool] $Recurse = $true, + + [switch] $CreateBackups, + + [switch] $Force, + + [switch] $NoRollbackOnMismatch, + + [int] $MaxDepth, + + [switch] $PassThru + ) + + # Validate paths + foreach ($singlePath in $Path) { + if (-not (Test-Path -LiteralPath $singlePath -PathType Container)) { + throw "Directory path '$singlePath' not found or is not a directory" + } + } + + # Define file type mappings + $fileTypeMappings = @{ + 'PowerShell' = @('.ps1', '.psm1', '.psd1', '.ps1xml') + 'CSharp' = @('.cs', '.csx', '.csproj') + 'Web' = @('.html', '.htm', '.css', '.js', '.json', '.xml', '.xsl', '.xslt') + 'Scripts' = @('.ps1', '.py', '.rb', '.sh', '.bash', '.bat', '.cmd') + 'Text' = @('.txt', '.md', '.log', '.config', '.ini', '.conf', '.yaml', '.yml') + 'All' = @('.ps1', '.psm1', '.psd1', '.ps1xml', '.cs', '.csx', '.html', '.htm', '.css', '.js', '.json', '.xml', '.txt', '.md', '.py', '.rb', '.sh', '.bat', '.cmd', '.config', '.ini', '.yaml', '.yml') + } + + # Determine target extensions + if ($PSCmdlet.ParameterSetName -eq 'FileType') { + $targetExtensions = $fileTypeMappings[$FileType] + Write-Verbose "Using $FileType file type: $($targetExtensions -join ', ')" + } else { + # Normalize extensions (ensure they start with a dot) + $targetExtensions = $Extensions | ForEach-Object { + if ($_.StartsWith('.')) { $_ } else { ".$_" } + } + Write-Verbose "Target extensions: $($targetExtensions -join ', ')" + } + + # Collect all files to process + $allFiles = @() + $summary = @{ + TotalDirectories = $Path.Count + ProcessedDirectories = 0 + TotalFiles = 0 + ProcessedFiles = 0 + ConvertedFiles = 0 + SkippedFiles = 0 + ErrorFiles = 0 + StartTime = Get-Date + } + + Write-Verbose "Scanning directories for files..." + + foreach ($singlePath in $Path) { + Write-Verbose "Processing directory: $singlePath" + $summary.ProcessedDirectories++ + + try { + $gciParams = @{ + LiteralPath = $singlePath + File = $true + } + + if ($Recurse) { + $gciParams.Recurse = $true + if ($MaxDepth) { $gciParams.Depth = $MaxDepth } + } + + $directoryFiles = Get-ChildItem @gciParams | Where-Object { + # Filter by extension + $extension = $_.Extension.ToLower() + $extensionMatch = $targetExtensions -contains $extension + + # Exclude directories if specified + $directoryExcluded = $false + if ($ExcludeDirectories -and $_.DirectoryName) { + $relativePath = $_.DirectoryName.Replace($singlePath, '').TrimStart('\', '/') + $directoryExcluded = $ExcludeDirectories | Where-Object { $relativePath -like "*$_*" } + } + + return $extensionMatch -and -not $directoryExcluded + } + + $allFiles += $directoryFiles + $summary.TotalFiles += $directoryFiles.Count + + Write-Verbose "Found $($directoryFiles.Count) matching files in $singlePath" + + } catch { + Write-Error "Error processing directory '$singlePath': $($_.Exception.Message)" + continue + } + } + + if ($allFiles.Count -eq 0) { + Write-Warning "No files found matching the specified criteria." + Write-Verbose "Extensions searched: $($targetExtensions -join ', ')" + Write-Verbose "Paths searched: $($Path -join ', ')" + return + } + + Write-Verbose "Found $($allFiles.Count) files across $($summary.ProcessedDirectories) directories" + Write-Verbose "Target extensions: $($targetExtensions -join ', ')" + Write-Verbose "Converting: $SourceEncoding → $TargetEncoding" + + if ($PSCmdlet.ShouldProcess("$($allFiles.Count) files", "Convert encoding from $SourceEncoding to $TargetEncoding")) { + + $results = @() + $progressCounter = 0 + + foreach ($file in $allFiles) { + $progressCounter++ + $percentComplete = [math]::Round(($progressCounter / $allFiles.Count) * 100, 1) + + Write-Progress -Activity "Converting file encodings" -Status "Processing $($file.Name) ($progressCounter of $($allFiles.Count))" -PercentComplete $percentComplete + + try { + # Create backup if requested + if ($CreateBackups) { + $backupPath = "$($file.FullName).bak" + Copy-Item -LiteralPath $file.FullName -Destination $backupPath -Force + Write-Verbose "Created backup: $backupPath" + } + + # Convert the file + $convertParams = @{ + Path = $file.FullName + SourceEncoding = $SourceEncoding + TargetEncoding = $TargetEncoding + Force = $Force + NoRollbackOnMismatch = $NoRollbackOnMismatch + WhatIf = $WhatIfPreference + } + + Convert-FileEncoding @convertParams + + $summary.ProcessedFiles++ + $summary.ConvertedFiles++ + + if ($PassThru) { + $results += [PSCustomObject]@{ + FilePath = $file.FullName + Extension = $file.Extension + Status = 'Converted' + BackupCreated = $CreateBackups + } + } + + } catch { + $summary.ErrorFiles++ + Write-Error "Error converting '$($file.FullName)': $($_.Exception.Message)" + + if ($PassThru) { + $results += [PSCustomObject]@{ + FilePath = $file.FullName + Extension = $file.Extension + Status = 'Error' + Error = $_.Exception.Message + } + } + } + } + + Write-Progress -Activity "Converting file encodings" -Completed + } + + # Display summary + $summary.EndTime = Get-Date + $summary.Duration = $summary.EndTime - $summary.StartTime + + Write-Verbose "Conversion Summary:" + Write-Verbose " Directories processed: $($summary.ProcessedDirectories)" + Write-Verbose " Files found: $($summary.TotalFiles)" + Write-Verbose " Files processed: $($summary.ProcessedFiles)" + Write-Verbose " Files converted: $($summary.ConvertedFiles)" + Write-Verbose " Files with errors: $($summary.ErrorFiles)" + Write-Verbose " Duration: $($summary.Duration.TotalSeconds.ToString('F2')) seconds" + + if ($CreateBackups -and $summary.ConvertedFiles -gt 0) { + Write-Verbose " Backups created with .bak extension" + } + + if ($PassThru) { + return $results + } +} diff --git a/Private/Convert-LineEnding.ps1 b/Private/Convert-LineEnding.ps1 new file mode 100644 index 00000000..b46e8516 --- /dev/null +++ b/Private/Convert-LineEnding.ps1 @@ -0,0 +1,77 @@ +function Convert-LineEndingSingle { + param( + [string] $FilePath, + [string] $TargetLineEnding, + [hashtable] $CurrentInfo, + [bool] $CreateBackup, + [bool] $EnsureFinalNewline + ) + + try { + # Read file content as string + $content = [System.IO.File]::ReadAllText($FilePath) + + if ([string]::IsNullOrEmpty($content)) { + return @{ + Status = 'Skipped' + Reason = 'Empty file' + } + } + + # Create backup if requested + $backupPath = $null + if ($CreateBackup) { + $backupPath = "$FilePath.backup" + $counter = 1 + while (Test-Path $backupPath) { + $backupPath = "$FilePath.backup$counter" + $counter++ + } + $originalBytes = [System.IO.File]::ReadAllBytes($FilePath) + [System.IO.File]::WriteAllBytes($backupPath, $originalBytes) + } + + # Normalize line endings first (convert all to LF) + $normalizedContent = $content -replace "`r`n", "`n" -replace "`r", "`n" + + # Convert to target line ending + $convertedContent = if ($TargetLineEnding -eq 'CRLF') { + $normalizedContent -replace "`n", "`r`n" + } else { + $normalizedContent + } + + # Ensure final newline if requested + if ($EnsureFinalNewline -and -not [string]::IsNullOrEmpty($convertedContent)) { + $targetNewline = if ($TargetLineEnding -eq 'CRLF') { "`r`n" } else { "`n" } + if (-not $convertedContent.EndsWith($targetNewline)) { + $convertedContent += $targetNewline + } + } + + # Write the converted content + $encoding = Get-FileEncoding -Path $FilePath -AsObject + [System.IO.File]::WriteAllText($FilePath, $convertedContent, $encoding.Encoding) + + $changesMade = @() + if ($CurrentInfo.LineEnding -ne $TargetLineEnding -and $CurrentInfo.LineEnding -ne 'None') { + $changesMade += "line endings ($($CurrentInfo.LineEnding) → $TargetLineEnding)" + } + if ($EnsureFinalNewline -and -not $CurrentInfo.HasFinalNewline) { + $changesMade += "added final newline" + } + + return @{ + Status = 'Converted' + Reason = "Converted: $($changesMade -join ', ')" + BackupPath = $backupPath + } + + } catch { + return @{ + Status = 'Error' + Reason = "Failed to convert: $_" + BackupPath = $backupPath + } + } +} \ No newline at end of file diff --git a/Private/Get-CurrentLineEnding.ps1 b/Private/Get-CurrentLineEnding.ps1 new file mode 100644 index 00000000..c555146c --- /dev/null +++ b/Private/Get-CurrentLineEnding.ps1 @@ -0,0 +1,71 @@ +function Get-CurrentLineEnding { + param([string] $FilePath) + + try { + $bytes = [System.IO.File]::ReadAllBytes($FilePath) + if ($bytes.Length -eq 0) { + return @{ + LineEnding = 'None' + HasFinalNewline = $true + } + } + + $crlfCount = 0 + $lfOnlyCount = 0 + $crOnlyCount = 0 + $hasFinalNewline = $false + + # Check if file ends with a newline + $lastByte = $bytes[$bytes.Length - 1] + if ($lastByte -eq 10) { + $hasFinalNewline = $true + } elseif ($lastByte -eq 13) { + $hasFinalNewline = $true + } + + for ($i = 0; $i -lt $bytes.Length - 1; $i++) { + if ($bytes[$i] -eq 13 -and $bytes[$i + 1] -eq 10) { + $crlfCount++ + $i++ + } elseif ($bytes[$i] -eq 10) { + $lfOnlyCount++ + } elseif ($bytes[$i] -eq 13) { + if ($i + 1 -lt $bytes.Length -and $bytes[$i + 1] -ne 10) { + $crOnlyCount++ + } + } + } + + if ($bytes.Length -gt 0) { + $lastByte = $bytes[$bytes.Length - 1] + if ($lastByte -eq 10 -and ($bytes.Length -eq 1 -or $bytes[$bytes.Length - 2] -ne 13)) { + $lfOnlyCount++ + } elseif ($lastByte -eq 13) { + $crOnlyCount++ + } + } + + $typesFound = @() + if ($crlfCount -gt 0) { $typesFound += 'CRLF' } + if ($lfOnlyCount -gt 0) { $typesFound += 'LF' } + if ($crOnlyCount -gt 0) { $typesFound += 'CR' } + + $lineEndingType = if ($typesFound.Count -eq 0) { + 'None' + } elseif ($typesFound.Count -eq 1) { + $typesFound[0] + } else { + 'Mixed' + } + + return @{ + LineEnding = $lineEndingType + HasFinalNewline = $hasFinalNewline + } + } catch { + return @{ + LineEnding = 'Error' + HasFinalNewline = $false + } + } +} \ No newline at end of file diff --git a/Private/Get-Encoding.ps1 b/Private/Get-Encoding.ps1 deleted file mode 100644 index b2c90149..00000000 --- a/Private/Get-Encoding.ps1 +++ /dev/null @@ -1,32 +0,0 @@ -function Get-Encoding { - [cmdletBinding()] - param ( - [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)][Alias('FullName')][string] $Path - ) - process { - $bom = New-Object -TypeName System.Byte[](4) - - $file = New-Object System.IO.FileStream($Path, 'Open', 'Read') - - $null = $file.Read($bom, 0, 4) - $file.Close() - $file.Dispose() - - $enc = [Text.Encoding]::ASCII - if ($bom[0] -eq 0x2b -and $bom[1] -eq 0x2f -and $bom[2] -eq 0x76) - { $enc = [Text.Encoding]::UTF7 } - if ($bom[0] -eq 0xff -and $bom[1] -eq 0xfe) - { $enc = [Text.Encoding]::Unicode } - if ($bom[0] -eq 0xfe -and $bom[1] -eq 0xff) - { $enc = [Text.Encoding]::BigEndianUnicode } - if ($bom[0] -eq 0x00 -and $bom[1] -eq 0x00 -and $bom[2] -eq 0xfe -and $bom[3] -eq 0xff) - { $enc = [Text.Encoding]::UTF32 } - if ($bom[0] -eq 0xef -and $bom[1] -eq 0xbb -and $bom[2] -eq 0xbf) - { $enc = [Text.Encoding]::UTF8 } - - [PSCustomObject]@{ - Encoding = $enc - Path = $Path - } - } -} \ No newline at end of file diff --git a/Private/Get-FileEncoding.ps1 b/Private/Get-FileEncoding.ps1 new file mode 100644 index 00000000..815233b3 --- /dev/null +++ b/Private/Get-FileEncoding.ps1 @@ -0,0 +1,111 @@ +function Get-FileEncoding { + <# + .SYNOPSIS + Get the encoding of a file (ASCII, UTF8, UTF8BOM, Unicode, BigEndianUnicode, UTF7, UTF32). + + .DESCRIPTION + Detects the encoding of a file using its byte order mark or by scanning for non‑ASCII characters. + Encoding is determined by the first few bytes of the file (BOM) or by the presence of non-ASCII characters. + Returns a string with the encoding name or a custom object when -AsObject is used. + + .PARAMETER Path + Path to the file to check. Supports pipeline input and can accept FullName property from Get-ChildItem. + + .PARAMETER AsObject + Returns a custom object with Path, Encoding, and EncodingName properties instead of just the encoding name string. + + .EXAMPLE + Get-FileEncoding -Path 'C:\temp\test.txt' + Returns the encoding name as a string (e.g., 'UTF8BOM', 'ASCII', 'Unicode') + + .EXAMPLE + Get-FileEncoding -Path 'C:\temp\test.txt' -AsObject + Returns a custom object with detailed encoding information + + .EXAMPLE + Get-ChildItem -Path 'C:\temp\*.txt' | Get-FileEncoding + Gets encoding for all text files in the directory via pipeline + + .NOTES + Supported encodings: ASCII, UTF8, UTF8BOM, Unicode (UTF-16LE), BigEndianUnicode (UTF-16BE), UTF7, UTF32 + #> + [CmdletBinding()] + param( + [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)] + [Alias('FullName')] + [string] $Path, + + [switch] $AsObject + ) + process { + if (-not (Test-Path -LiteralPath $Path)) { + $msg = "Get-FileEncoding - File not found: $Path" + if ($ErrorActionPreference -eq 'Stop') { throw $msg } + Write-Warning $msg + return + } + + $fs = [System.IO.FileStream]::new($Path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read) + try { + $bom = [byte[]]::new(4) + $null = $fs.Read($bom, 0, 4) + $enc = [System.Text.Encoding]::ASCII + + # Check for BOMs in order of specificity (longer BOMs first) + if ($bom[0] -eq 0x00 -and $bom[1] -eq 0x00 -and $bom[2] -eq 0xfe -and $bom[3] -eq 0xff) { + $enc = [System.Text.Encoding]::UTF32 + } elseif ($bom[0] -eq 0xef -and $bom[1] -eq 0xbb -and $bom[2] -eq 0xbf) { + $enc = [System.Text.UTF8Encoding]::new($true) + } elseif ($bom[0] -eq 0xff -and $bom[1] -eq 0xfe) { + $enc = [System.Text.Encoding]::Unicode + } elseif ($bom[0] -eq 0xfe -and $bom[1] -eq 0xff) { + $enc = [System.Text.Encoding]::BigEndianUnicode + } elseif ($bom[0] -eq 0x2b -and $bom[1] -eq 0x2f -and $bom[2] -eq 0x76) { + $enc = [System.Text.Encoding]::UTF7 + } else { + # No BOM detected - scan for non-ASCII characters to distinguish UTF8 from ASCII + $fs.Position = 0 + $byte = [byte[]]::new(1) + while ($fs.Read($byte, 0, 1) -gt 0) { + if ($byte[0] -gt 0x7F) { + $enc = [System.Text.UTF8Encoding]::new($false) + break + } + } + } + } finally { + $fs.Close() + $fs.Dispose() + } + + # Determine encoding name for consistent return values + $encName = if ($enc -is [System.Text.UTF8Encoding] -and $enc.GetPreamble().Length -eq 3) { + 'UTF8BOM' + } elseif ($enc -is [System.Text.UTF8Encoding]) { + 'UTF8' + } elseif ($enc -is [System.Text.UnicodeEncoding]) { + 'Unicode' + } elseif ($enc -is [System.Text.UTF7Encoding]) { + 'UTF7' + } elseif ($enc -is [System.Text.UTF32Encoding]) { + 'UTF32' + } elseif ($enc -is [System.Text.ASCIIEncoding]) { + 'ASCII' + } elseif ($enc -is [System.Text.BigEndianUnicodeEncoding]) { + 'BigEndianUnicode' + } else { + $enc.WebName + } + + if ($AsObject) { + [PSCustomObject]@{ + Path = $Path + Encoding = $enc + EncodingName = $encName + } + } else { + $encName + } + } +} + diff --git a/Private/Get-FolderEncoding.ps1 b/Private/Get-FolderEncoding.ps1 new file mode 100644 index 00000000..5db0e3af --- /dev/null +++ b/Private/Get-FolderEncoding.ps1 @@ -0,0 +1,416 @@ +function Get-FolderEncoding { + <# + .SYNOPSIS + Analyzes file encodings in folders based on file extensions. + + .DESCRIPTION + A user-friendly wrapper that analyzes encoding distribution across files in one or more folders, + filtered by file extensions. This function is ideal for understanding the current encoding state + of specific file types before performing conversions. + + The function supports both single and multiple file extensions, with predefined file type groups + for common scenarios. It provides comprehensive analysis including encoding distribution, + inconsistencies, and recommendations. + + .PARAMETER Path + The directory path to analyze. Can be a single directory or an array of directories. + Use '.' for the current directory. + + .PARAMETER Extensions + File extensions to analyze. Can be specified with or without the leading dot. + Examples: 'ps1', '.ps1', @('ps1', 'psm1'), @('.cs', '.vb') + + Common presets available via -FileType parameter for convenience. + + .PARAMETER FileType + Predefined file type groups for common scenarios: + - PowerShell: .ps1, .psm1, .psd1, .ps1xml + - CSharp: .cs, .csx + - Web: .html, .css, .js, .json, .xml + - Scripts: .ps1, .py, .rb, .sh, .bat, .cmd + - Text: .txt, .md, .log, .config + - All: Analyzes all common text file types + + .PARAMETER ExcludeDirectories + Directory names to exclude from analysis (e.g., '.git', 'bin', 'obj', 'node_modules'). + Default excludes common build and version control directories. + + .PARAMETER Recurse + Process files in subdirectories recursively. Default is $true. + + .PARAMETER MaxDepth + Maximum directory depth to recurse when -Recurse is enabled. Default is unlimited. + + .PARAMETER GroupByExtension + Group results by file extension to show encoding distribution per file type. + + .PARAMETER ShowFiles + Include individual file details in the output. Default is $true. + Use -ShowFiles:$false to disable if you only want summary statistics. + + .PARAMETER RecommendTarget + Provide encoding recommendations based on file types (e.g., UTF8BOM for PowerShell files). + Default is $true. Use -RecommendTarget:$false to disable recommendations. + + .EXAMPLE + Get-FolderEncoding -Path . -Extensions 'ps1' + + Analyze PowerShell files in the current directory with file details and recommendations (default behavior). + + .EXAMPLE + Get-FolderEncoding -Path @('.\Scripts', '.\Modules') -FileType PowerShell -GroupByExtension + + Analyze all PowerShell files in Scripts and Modules directories, grouped by extension. + + .EXAMPLE + Get-FolderEncoding -Path . -Extensions @('cs', 'vb') -ShowFiles:$false + + Analyze C# and VB.NET files showing only summary statistics, no individual file details. + + .EXAMPLE + Get-FolderEncoding -Path .\Source -FileType Web -ExcludeDirectories @('node_modules', 'dist') + + Analyze web files, excluding build directories, with full details and recommendations. + + .OUTPUTS + PSCustomObject with the following properties: + - Summary: Overall statistics and recommendations + - EncodingDistribution: Hashtable of encoding counts + - ExtensionAnalysis: Analysis grouped by file extension (if -GroupByExtension) + - Files: Individual file details (default: included) + - Recommendations: Encoding recommendations (default: included) + + .NOTES + Author: PowerShell Encoding Tools + + This function provides analysis capabilities to complement Convert-FolderEncoding. + Use this to understand your current encoding state before performing conversions. + + PowerShell Encoding Notes: + - UTF8BOM is recommended for PowerShell files to ensure PS 5.1 compatibility + - Mixed encodings within a project can cause issues + - ASCII files are compatible with UTF8 but may need BOM for PowerShell + #> + [CmdletBinding(DefaultParameterSetName = 'Extensions')] + param( + [Parameter(Mandatory)] + [Alias('Directory', 'Folder')] + [string[]] $Path, + + [Parameter(ParameterSetName = 'Extensions', Mandatory)] + [string[]] $Extensions, + + [Parameter(ParameterSetName = 'FileType', Mandatory)] + [ValidateSet('PowerShell', 'CSharp', 'Web', 'Scripts', 'Text', 'All')] + [string] $FileType, + + [string[]] $ExcludeDirectories = @('.git', '.vs', 'bin', 'obj', 'packages', 'node_modules', '.vscode', 'dist', 'build'), + + [bool] $Recurse = $true, + + [int] $MaxDepth, + + [switch] $GroupByExtension, + + [bool] $ShowFiles = $true, + + [bool] $RecommendTarget = $true + ) + + # Validate paths + foreach ($singlePath in $Path) { + if (-not (Test-Path -LiteralPath $singlePath -PathType Container)) { + throw "Directory path '$singlePath' not found or is not a directory" + } + } + + # Define file type mappings + $fileTypeMappings = @{ + 'PowerShell' = @('.ps1', '.psm1', '.psd1', '.ps1xml') + 'CSharp' = @('.cs', '.csx', '.csproj') + 'Web' = @('.html', '.htm', '.css', '.js', '.json', '.xml', '.xsl', '.xslt') + 'Scripts' = @('.ps1', '.py', '.rb', '.sh', '.bash', '.bat', '.cmd') + 'Text' = @('.txt', '.md', '.log', '.config', '.ini', '.conf', '.yaml', '.yml') + 'All' = @('.ps1', '.psm1', '.psd1', '.ps1xml', '.cs', '.csx', '.html', '.htm', '.css', '.js', '.json', '.xml', '.txt', '.md', '.py', '.rb', '.sh', '.bat', '.cmd', '.config', '.ini', '.yaml', '.yml') + } + + # Define encoding recommendations by file type + $encodingRecommendations = @{ + '.ps1' = 'UTF8BOM' + '.psm1' = 'UTF8BOM' + '.psd1' = 'UTF8BOM' + '.ps1xml' = 'UTF8BOM' + '.cs' = 'UTF8' + '.csx' = 'UTF8' + '.html' = 'UTF8' + '.htm' = 'UTF8' + '.css' = 'UTF8' + '.js' = 'UTF8' + '.json' = 'UTF8' + '.xml' = 'UTF8' + '.txt' = 'UTF8' + '.md' = 'UTF8' + '.py' = 'UTF8' + '.rb' = 'UTF8' + '.sh' = 'UTF8' + '.bat' = 'UTF8' + '.cmd' = 'UTF8' + '.config' = 'UTF8' + '.ini' = 'UTF8' + '.yaml' = 'UTF8' + '.yml' = 'UTF8' + } + + # Determine target extensions + if ($PSCmdlet.ParameterSetName -eq 'FileType') { + $targetExtensions = $fileTypeMappings[$FileType] + Write-Verbose "Analyzing $FileType file type: $($targetExtensions -join ', ')" + } else { + # Normalize extensions (ensure they start with a dot) + $targetExtensions = $Extensions | ForEach-Object { + if ($_.StartsWith('.')) { $_ } else { ".$_" } + } + Write-Verbose "Target extensions: $($targetExtensions -join ', ')" + } + + # Load encoding detection function + if (-not (Get-Command Get-FileEncoding -ErrorAction SilentlyContinue)) { + throw "Get-FileEncoding function not found. Please ensure the encoding detection module is loaded." + } + + # Collect all files to analyze + $allFiles = @() + $summary = @{ + TotalDirectories = $Path.Count + ProcessedDirectories = 0 + TotalFiles = 0 + StartTime = Get-Date + } + + Write-Verbose "Scanning directories for files..." + + foreach ($singlePath in $Path) { + Write-Verbose "Processing directory: $singlePath" + $summary.ProcessedDirectories++ + + try { + $gciParams = @{ + LiteralPath = $singlePath + File = $true + } + + if ($Recurse) { + $gciParams.Recurse = $true + if ($MaxDepth) { $gciParams.Depth = $MaxDepth } + } + + $directoryFiles = Get-ChildItem @gciParams | Where-Object { + # Filter by extension + $extension = $_.Extension.ToLower() + $extensionMatch = $targetExtensions -contains $extension + + # Exclude directories if specified + $directoryExcluded = $false + if ($ExcludeDirectories -and $_.DirectoryName) { + $relativePath = $_.DirectoryName.Replace($singlePath, '').TrimStart('\', '/') + $directoryExcluded = $ExcludeDirectories | Where-Object { $relativePath -like "*$_*" } + } + + return $extensionMatch -and -not $directoryExcluded + } + + $allFiles += $directoryFiles + $summary.TotalFiles += $directoryFiles.Count + + Write-Verbose "Found $($directoryFiles.Count) matching files in $singlePath" + + } catch { + Write-Error "Error processing directory '$singlePath': $($_.Exception.Message)" + continue + } + } + + if ($allFiles.Count -eq 0) { + Write-Warning "No files found matching the specified criteria." + Write-Verbose "Extensions searched: $($targetExtensions -join ', ')" + Write-Verbose "Paths searched: $($Path -join ', ')" + return + } + + Write-Verbose "Analyzing $($allFiles.Count) files across $($summary.ProcessedDirectories) directories" + + # Analyze file encodings + $encodingDistribution = @{} + $extensionAnalysis = @{} + $fileDetails = @() + $inconsistentExtensions = @() + + $progressCounter = 0 + foreach ($file in $allFiles) { + $progressCounter++ + $percentComplete = [math]::Round(($progressCounter / $allFiles.Count) * 100, 1) + + Write-Progress -Activity "Analyzing file encodings" -Status "Processing $($file.Name) ($progressCounter of $($allFiles.Count))" -PercentComplete $percentComplete + + try { + $encoding = Get-FileEncoding -Path $file.FullName + $extension = $file.Extension.ToLower() + + # Update encoding distribution + if ($encodingDistribution.ContainsKey($encoding)) { + $encodingDistribution[$encoding]++ + } else { + $encodingDistribution[$encoding] = 1 + } + + # Update extension analysis + if (-not $extensionAnalysis.ContainsKey($extension)) { + $extensionAnalysis[$extension] = @{} + } + if ($extensionAnalysis[$extension].ContainsKey($encoding)) { + $extensionAnalysis[$extension][$encoding]++ + } else { + $extensionAnalysis[$extension][$encoding] = 1 + } + + # Store file details if requested + if ($ShowFiles) { + $recommendedEncoding = if ($RecommendTarget -and $encodingRecommendations.ContainsKey($extension)) { + $encodingRecommendations[$extension] + } else { + $null + } + + $fileDetails += [PSCustomObject]@{ + FullPath = $file.FullName + RelativePath = $file.FullName.Replace((Get-Location).Path, '.').TrimStart('\', '/') + Extension = $extension + CurrentEncoding = $encoding + RecommendedEncoding = $recommendedEncoding + NeedsConversion = $recommendedEncoding -and ($encoding -ne $recommendedEncoding) + Size = $file.Length + LastModified = $file.LastWriteTime + } + } + + } catch { + Write-Warning "Could not analyze encoding for '$($file.FullName)': $($_.Exception.Message)" + continue + } + } + + Write-Progress -Activity "Analyzing file encodings" -Completed + + # Identify extensions with mixed encodings + foreach ($ext in $extensionAnalysis.Keys) { + if ($extensionAnalysis[$ext].Keys.Count -gt 1) { + $inconsistentExtensions += $ext + } + } + + # Generate recommendations + $recommendations = @() + if ($RecommendTarget) { + foreach ($ext in $targetExtensions) { + $recommendedEncoding = $encodingRecommendations[$ext] + if ($recommendedEncoding -and $extensionAnalysis.ContainsKey($ext)) { + $currentEncodings = $extensionAnalysis[$ext] + $needsConversion = $currentEncodings.Keys | Where-Object { $_ -ne $recommendedEncoding } + + if ($needsConversion) { + $totalFiles = ($currentEncodings.Values | Measure-Object -Sum).Sum + $nonCompliantFiles = $needsConversion | ForEach-Object { $currentEncodings[$_] } | Measure-Object -Sum | Select-Object -ExpandProperty Sum + + $recommendations += [PSCustomObject]@{ + Extension = $ext + RecommendedEncoding = $recommendedEncoding + TotalFiles = $totalFiles + CompliantFiles = $totalFiles - $nonCompliantFiles + NonCompliantFiles = $nonCompliantFiles + CurrentEncodings = $currentEncodings + } + } + } + } + } + + # Build summary + $summary.EndTime = Get-Date + $summary.Duration = $summary.EndTime - $summary.StartTime + $summary.UniqueEncodings = $encodingDistribution.Keys.Count + $summary.MostCommonEncoding = ($encodingDistribution.GetEnumerator() | Sort-Object Value -Descending | Select-Object -First 1).Key + $summary.HasMixedEncodings = $inconsistentExtensions.Count -gt 0 + $summary.InconsistentExtensions = $inconsistentExtensions + + Write-Verbose "Analysis completed: $($summary.TotalFiles) files, $($summary.UniqueEncodings) unique encodings" + Write-Verbose "Most common encoding: $($summary.MostCommonEncoding)" + if ($summary.HasMixedEncodings) { + Write-Verbose "Extensions with mixed encodings: $($inconsistentExtensions -join ', ')" + } + + # Build result object + $result = [PSCustomObject]@{ + Summary = [PSCustomObject]@{ + TotalDirectories = $summary.TotalDirectories + ProcessedDirectories = $summary.ProcessedDirectories + TotalFiles = $summary.TotalFiles + UniqueEncodings = $summary.UniqueEncodings + MostCommonEncoding = $summary.MostCommonEncoding + HasMixedEncodings = $summary.HasMixedEncodings + InconsistentExtensions = $summary.InconsistentExtensions + Duration = $summary.Duration + StartTime = $summary.StartTime + EndTime = $summary.EndTime + } + EncodingDistribution = $encodingDistribution + Files = if ($ShowFiles) { $fileDetails } else { $null } + Recommendations = if ($RecommendTarget) { $recommendations } else { $null } + } + + if ($GroupByExtension) { + $result | Add-Member -NotePropertyName 'ExtensionAnalysis' -NotePropertyValue $extensionAnalysis + } + + # Add a display summary method for better default output + $result | Add-Member -MemberType ScriptMethod -Name 'DisplaySummary' -Value { + Write-Host "" + Write-Host "📊 Folder Encoding Analysis Summary" -ForegroundColor Green + Write-Host "==================================" + Write-Host "📁 Directories analyzed: $($this.Summary.ProcessedDirectories)" -ForegroundColor Cyan + Write-Host "📄 Total files found: $($this.Summary.TotalFiles)" -ForegroundColor Cyan + Write-Host "🔤 Unique encodings: $($this.Summary.UniqueEncodings)" -ForegroundColor Cyan + Write-Host "⭐ Most common encoding: $($this.Summary.MostCommonEncoding)" -ForegroundColor Green + Write-Host "⚠️ Mixed encodings: $($this.Summary.HasMixedEncodings)" -ForegroundColor $(if ($this.Summary.HasMixedEncodings) { 'Yellow' } else { 'Green' }) + + if ($this.Summary.HasMixedEncodings) { + Write-Host "📝 Inconsistent extensions: $($this.Summary.InconsistentExtensions -join ', ')" -ForegroundColor Yellow + } + + Write-Host "" + Write-Host "📈 Encoding Distribution:" -ForegroundColor Blue + $this.EncodingDistribution.GetEnumerator() | Sort-Object Value -Descending | ForEach-Object { + $percentage = [math]::Round(($_.Value / $this.Summary.TotalFiles) * 100, 1) + Write-Host " $($_.Key): $($_.Value) files ($percentage%)" -ForegroundColor White + } + + if ($this.Recommendations -and $this.Recommendations.Count -gt 0) { + Write-Host "" + Write-Host "💡 Recommendations:" -ForegroundColor Magenta + foreach ($rec in $this.Recommendations) { + Write-Host " $($rec.Extension) files:" -ForegroundColor White + Write-Host " Target encoding: $($rec.RecommendedEncoding)" -ForegroundColor Green + Write-Host " Files needing conversion: $($rec.NonCompliantFiles)/$($rec.TotalFiles)" -ForegroundColor Yellow + } + } + + Write-Host "" + Write-Host "⏱️ Analysis duration: $($this.Summary.Duration.TotalSeconds.ToString('F2')) seconds" -ForegroundColor Gray + } + + # Add a custom ToString method for better default display + $result | Add-Member -MemberType ScriptMethod -Name 'ToString' -Value { + return "Folder Encoding Analysis: $($this.Summary.TotalFiles) files, $($this.Summary.UniqueEncodings) encodings, Most common: $($this.Summary.MostCommonEncoding)" + } -Force + + return $result +} diff --git a/Private/Get-LineEndingType.ps1 b/Private/Get-LineEndingType.ps1 new file mode 100644 index 00000000..ec86a6cd --- /dev/null +++ b/Private/Get-LineEndingType.ps1 @@ -0,0 +1,85 @@ +function Get-LineEndingType { + param([string] $FilePath) + + try { + $bytes = [System.IO.File]::ReadAllBytes($FilePath) + if ($bytes.Length -eq 0) { + return @{ + LineEnding = 'None' + HasFinalNewline = $true # Empty files are considered OK + FileSize = 0 + } + } + + $crlfCount = 0 + $lfOnlyCount = 0 + $crOnlyCount = 0 + $hasFinalNewline = $false + + # Check if file ends with a newline + $lastByte = $bytes[$bytes.Length - 1] + if ($lastByte -eq 10) { + # Ends with LF + $hasFinalNewline = $true + if ($bytes.Length -gt 1 -and $bytes[$bytes.Length - 2] -eq 13) { + # Actually ends with CRLF + } + } elseif ($lastByte -eq 13) { + # Ends with CR + $hasFinalNewline = $true + } + + for ($i = 0; $i -lt $bytes.Length - 1; $i++) { + if ($bytes[$i] -eq 13 -and $bytes[$i + 1] -eq 10) { + # CRLF found + $crlfCount++ + $i++ # Skip the LF part + } elseif ($bytes[$i] -eq 10) { + # LF only + $lfOnlyCount++ + } elseif ($bytes[$i] -eq 13) { + # CR only (check if not followed by LF) + if ($i + 1 -lt $bytes.Length -and $bytes[$i + 1] -ne 10) { + $crOnlyCount++ + } + } + } + + # Check last byte for standalone LF or CR (if not already counted) + if ($bytes.Length -gt 0) { + $lastByte = $bytes[$bytes.Length - 1] + if ($lastByte -eq 10 -and ($bytes.Length -eq 1 -or $bytes[$bytes.Length - 2] -ne 13)) { + $lfOnlyCount++ + } elseif ($lastByte -eq 13) { + $crOnlyCount++ + } + } + + # Determine line ending type + $typesFound = @() + if ($crlfCount -gt 0) { $typesFound += 'CRLF' } + if ($lfOnlyCount -gt 0) { $typesFound += 'LF' } + if ($crOnlyCount -gt 0) { $typesFound += 'CR' } + + $lineEndingType = if ($typesFound.Count -eq 0) { + 'None' + } elseif ($typesFound.Count -eq 1) { + $typesFound[0] + } else { + 'Mixed' + } + + return @{ + LineEnding = $lineEndingType + HasFinalNewline = $hasFinalNewline + FileSize = $bytes.Length + } + + } catch { + return @{ + LineEnding = 'Error' + HasFinalNewline = $false + FileSize = 0 + } + } +} \ No newline at end of file diff --git a/Private/Get-RelativePath.ps1 b/Private/Get-RelativePath.ps1 new file mode 100644 index 00000000..717f6bc4 --- /dev/null +++ b/Private/Get-RelativePath.ps1 @@ -0,0 +1,57 @@ +function Get-RelativePath { + <# + .SYNOPSIS + Gets the relative path from one path to another, compatible with PowerShell 5.1. + + .DESCRIPTION + Provides PowerShell 5.1 compatible relative path calculation that works like + [System.IO.Path]::GetRelativePath() which is only available in .NET Core 2.0+. + + .PARAMETER From + The base path to calculate the relative path from. + + .PARAMETER To + The target path to calculate the relative path to. + + .EXAMPLE + Get-RelativePath -From 'C:\Projects' -To 'C:\Projects\MyProject\file.txt' + Returns: MyProject\file.txt + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string] $From, + + [Parameter(Mandatory)] + [string] $To + ) + + # Use .NET Core method if available (PowerShell 7+) + if ([System.IO.Path].GetMethods() | Where-Object { $_.Name -eq 'GetRelativePath' -and $_.IsStatic }) { + return [System.IO.Path]::GetRelativePath($From, $To) + } + + # PowerShell 5.1 compatible implementation + try { + # Use New-Object for PS 5.1 compatibility instead of ::new() + $fromPath = [System.IO.Path]::GetFullPath($From) + if (-not $fromPath.EndsWith([System.IO.Path]::DirectorySeparatorChar)) { + $fromPath += [System.IO.Path]::DirectorySeparatorChar + } + $fromUri = New-Object System.Uri $fromPath + $toUri = New-Object System.Uri ([System.IO.Path]::GetFullPath($To)) + + $relativeUri = $fromUri.MakeRelativeUri($toUri) + $relativePath = [System.Uri]::UnescapeDataString($relativeUri.ToString()) + + # Convert forward slashes to backslashes on Windows + if ([System.IO.Path]::DirectorySeparatorChar -eq '\') { + $relativePath = $relativePath.Replace('/', '\') + } + + return $relativePath + } catch { + # Fallback: just return the filename if relative path calculation fails + return [System.IO.Path]::GetFileName($To) + } +} diff --git a/Private/Resolve-Encoding.ps1 b/Private/Resolve-Encoding.ps1 new file mode 100644 index 00000000..a40a3e08 --- /dev/null +++ b/Private/Resolve-Encoding.ps1 @@ -0,0 +1,56 @@ +function Resolve-Encoding { + <# + .SYNOPSIS + Resolves encoding names to .NET System.Text.Encoding objects. + + .DESCRIPTION + Converts encoding name strings to proper .NET encoding objects, with special handling + for UTF8BOM (UTF8 with BOM) and OEM encodings that aren't directly available in + System.Text.Encoding static properties. + + .PARAMETER Name + The name of the encoding to resolve. Supports common encodings used in text file processing. + 'Any' is a special value that returns null - used for converting from any encoding. + + .EXAMPLE + Resolve-Encoding -Name 'UTF8BOM' + Returns a UTF8Encoding object configured to emit a BOM. + + .EXAMPLE + Resolve-Encoding -Name 'ASCII' + Returns the ASCII encoding object. + + .EXAMPLE + Resolve-Encoding -Name 'Any' + Returns null - used to indicate "any source encoding" in conversion functions. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateSet('Ascii','BigEndianUnicode','Unicode','UTF7','UTF8','UTF8BOM','UTF32','Default','OEM','Any')] + [string] $Name + ) + + switch ($Name.ToUpperInvariant()) { + 'UTF8BOM' { + return [System.Text.UTF8Encoding]::new($true) + } + 'UTF8' { + return [System.Text.UTF8Encoding]::new($false) + } + 'OEM' { + return [System.Text.Encoding]::GetEncoding([Console]::OutputEncoding.CodePage) + } + 'ANY' { + # Return null for 'Any' - caller will handle this case + return $null + } + default { + try { + return [System.Text.Encoding]::$Name + } catch { + throw "Failed to resolve encoding '$Name': $($_.Exception.Message)" + } + } + } +} diff --git a/Public/Convert-ProjectEncoding.ps1 b/Public/Convert-ProjectEncoding.ps1 new file mode 100644 index 00000000..80d48b80 --- /dev/null +++ b/Public/Convert-ProjectEncoding.ps1 @@ -0,0 +1,309 @@ +function Convert-ProjectEncoding { + <# + .SYNOPSIS + Converts encoding for all source files in a project directory with comprehensive safety features. + + .DESCRIPTION + Recursively converts encoding for PowerShell, C#, and other source code files in a project directory. + Includes comprehensive safety features: WhatIf support, automatic backups, rollback protection, + and detailed reporting. Designed specifically for development projects with intelligent file type detection. + + .PARAMETER Path + Path to the project directory to process. + + .PARAMETER ProjectType + Type of project to process. Determines which file extensions are included. + Valid values: 'PowerShell', 'CSharp', 'Mixed', 'All', 'Custom' + + .PARAMETER CustomExtensions + Custom file extensions to process when ProjectType is 'Custom'. + Example: @('*.ps1', '*.psm1', '*.cs', '*.vb') + + .PARAMETER SourceEncoding + Expected source encoding of files. When specified, only files with this encoding will be converted. + When not specified (or set to 'Any'), files with any encoding except the target encoding will be converted. + + .PARAMETER TargetEncoding + Target encoding for conversion. + Default is 'UTF8BOM' for PowerShell projects (PS 5.1 compatibility), 'UTF8' for others. + + .PARAMETER ExcludeDirectories + Directory names to exclude from processing (e.g., '.git', 'bin', 'obj'). + + .PARAMETER CreateBackups + Create backup files before conversion for additional safety. + + .PARAMETER BackupDirectory + Directory to store backup files. If not specified, backups are created alongside original files. + + .PARAMETER Force + Convert files even when their detected encoding doesn't match SourceEncoding. + + .PARAMETER NoRollbackOnMismatch + Skip rolling back changes when content verification fails. + + .PARAMETER PassThru + Return detailed results for each processed file. + + .EXAMPLE + Convert-ProjectEncoding -Path 'C:\MyProject' -ProjectType PowerShell -WhatIf + Preview encoding conversion for a PowerShell project (will convert from ANY encoding to UTF8BOM by default). + + .EXAMPLE + Convert-ProjectEncoding -Path 'C:\MyProject' -ProjectType PowerShell -TargetEncoding UTF8BOM + Convert ALL files in a PowerShell project to UTF8BOM regardless of their current encoding. + + .EXAMPLE + Convert-ProjectEncoding -Path 'C:\MyProject' -ProjectType Mixed -SourceEncoding ASCII -TargetEncoding UTF8BOM -CreateBackups + Convert ONLY ASCII files in a mixed project to UTF8BOM with backups. + + .EXAMPLE + Convert-ProjectEncoding -Path 'C:\MyProject' -ProjectType CSharp -TargetEncoding UTF8 -PassThru + Convert ALL files in a C# project to UTF8 without BOM and return detailed results. + + .NOTES + File type mappings: + - PowerShell: *.ps1, *.psm1, *.psd1, *.ps1xml + - CSharp: *.cs, *.csx, *.csproj, *.sln, *.config, *.json, *.xml + - Mixed: Combination of PowerShell and CSharp + - All: Common source code extensions including JS, Python, etc. + + PowerShell Encoding Recommendations: + - UTF8BOM is recommended for PowerShell files to ensure PS 5.1 compatibility + - UTF8 without BOM can cause PS 5.1 to misinterpret files as ASCII + - This can lead to broken special characters and module loading issues + - UTF8BOM ensures proper encoding detection across all PowerShell versions + #> + [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'ProjectType')] + param( + [Parameter(Mandatory)] + [string] $Path, + + [Parameter(ParameterSetName = 'ProjectType')] + [ValidateSet('PowerShell', 'CSharp', 'Mixed', 'All')] + [string] $ProjectType = 'Mixed', + + [Parameter(ParameterSetName = 'Custom', Mandatory)] + [string[]] $CustomExtensions, + + [ValidateSet('Ascii', 'BigEndianUnicode', 'Unicode', 'UTF7', 'UTF8', 'UTF8BOM', 'UTF32', 'Default', 'OEM', 'Any')] + [string] $SourceEncoding = 'Any', + + [ValidateSet('Ascii', 'BigEndianUnicode', 'Unicode', 'UTF7', 'UTF8', 'UTF8BOM', 'UTF32', 'Default', 'OEM')] + [string] $TargetEncoding, + + [string[]] $ExcludeDirectories = @('.git', '.vs', 'bin', 'obj', 'packages', 'node_modules', '.vscode'), + + [switch] $CreateBackups, + + [string] $BackupDirectory, + + [switch] $Force, + [switch] $NoRollbackOnMismatch, + [switch] $PassThru + ) + + # Validate path + if (-not (Test-Path -LiteralPath $Path -PathType Container)) { + throw "Project path '$Path' not found or is not a directory" + } + + # Define file extension mappings + $extensionMappings = @{ + 'PowerShell' = @('*.ps1', '*.psm1', '*.psd1', '*.ps1xml') + 'CSharp' = @('*.cs', '*.csx', '*.csproj', '*.sln', '*.config', '*.json', '*.xml', '*.resx') + 'Mixed' = @('*.ps1', '*.psm1', '*.psd1', '*.ps1xml', '*.cs', '*.csx', '*.csproj', '*.sln', '*.config', '*.json', '*.xml') + 'All' = @('*.ps1', '*.psm1', '*.psd1', '*.ps1xml', '*.cs', '*.csx', '*.csproj', '*.sln', '*.config', '*.json', '*.xml', '*.js', '*.ts', '*.py', '*.rb', '*.java', '*.cpp', '*.h', '*.hpp', '*.sql', '*.md', '*.txt', '*.yaml', '*.yml') + } + + # Determine file patterns to process + if ($PSCmdlet.ParameterSetName -eq 'Custom') { + $filePatterns = $CustomExtensions + } else { + $filePatterns = $extensionMappings[$ProjectType] + } + + Write-Verbose "Processing project type: $ProjectType with patterns: $($filePatterns -join ', ')" + + # Set default TargetEncoding based on project type if not specified + if (-not $PSBoundParameters.ContainsKey('TargetEncoding')) { + switch ($ProjectType) { + 'PowerShell' { $TargetEncoding = 'UTF8BOM' } + 'Mixed' { $TargetEncoding = 'UTF8BOM' } # Mixed likely contains PowerShell files + default { $TargetEncoding = 'UTF8' } + } + Write-Verbose "Using default TargetEncoding '$TargetEncoding' for project type '$ProjectType'" + } + + # Prepare backup directory if specified + if ($CreateBackups -and $BackupDirectory) { + if (-not (Test-Path -LiteralPath $BackupDirectory)) { + New-Item -Path $BackupDirectory -ItemType Directory -Force | Out-Null + Write-Verbose "Created backup directory: $BackupDirectory" + } + } + + # Resolve encodings + $target = Resolve-Encoding -Name $TargetEncoding + + # For 'Any' source encoding, we'll handle it differently in the processing loop + $source = if ($SourceEncoding -eq 'Any') { $null } else { Resolve-Encoding -Name $SourceEncoding } + + # Collect all files to process + $allFiles = @() + + foreach ($pattern in $filePatterns) { + $params = @{ + Path = $Path + Filter = $pattern + File = $true + Recurse = $true + } + + $files = Get-ChildItem @params | Where-Object { + $file = $_ + $excluded = $false + + foreach ($excludeDir in $ExcludeDirectories) { + if ($file.DirectoryName -like "*\$excludeDir" -or $file.DirectoryName -like "*\$excludeDir\*") { + $excluded = $true + break + } + } + + -not $excluded + } + + $allFiles += $files + } + + # Remove duplicates (files matching multiple patterns) + $uniqueFiles = $allFiles | Sort-Object FullName | Get-Unique -AsString + + Write-Host "Found $($uniqueFiles.Count) files to process" -ForegroundColor Green + + if ($uniqueFiles.Count -eq 0) { + Write-Warning "No files found matching the specified criteria" + return + } + + # Process files + $results = @() + $converted = 0 + $skipped = 0 + $errors = 0 + + foreach ($file in $uniqueFiles) { + try { + # For 'Any' source encoding, handle differently + if ($SourceEncoding -eq 'Any') { + # Get the current file encoding + $currentEncodingObj = Get-FileEncoding -Path $file.FullName -AsObject + $currentEncodingName = $currentEncodingObj.EncodingName + + # Get target encoding name for comparison + $targetName = if ($target -is [System.Text.UTF8Encoding] -and $target.GetPreamble().Length -eq 3) { 'UTF8BOM' } + elseif ($target -is [System.Text.UTF8Encoding]) { 'UTF8' } + elseif ($target -is [System.Text.UnicodeEncoding]) { 'Unicode' } + elseif ($target -is [System.Text.UTF7Encoding]) { 'UTF7' } + elseif ($target -is [System.Text.UTF32Encoding]) { 'UTF32' } + elseif ($target -is [System.Text.ASCIIEncoding]) { 'ASCII' } + elseif ($target -is [System.Text.BigEndianUnicodeEncoding]) { 'BigEndianUnicode' } + else { $target.WebName } + + # Skip if already target encoding + if ($currentEncodingName -eq $targetName) { + Write-Verbose "Skipping $($file.FullName) because encoding is already '$targetName'." + $results += @{ + FilePath = $file.FullName + Status = 'Skipped' + Reason = "Already target encoding '$targetName'" + DetectedEncoding = $currentEncodingName + } + $skipped++ + continue + } + + # Use the detected encoding as source for conversion + $convertParams = @{ + FilePath = $file.FullName + SourceEncoding = $currentEncodingObj.Encoding + TargetEncoding = $target + Force = $true # Always force when using 'Any' + NoRollbackOnMismatch = $NoRollbackOnMismatch + WhatIf = $WhatIfPreference + } + } else { + # Original logic for specific source encoding + $convertParams = @{ + FilePath = $file.FullName + SourceEncoding = $source + TargetEncoding = $target + Force = $Force + NoRollbackOnMismatch = $NoRollbackOnMismatch + WhatIf = $WhatIfPreference + } + } + + if ($CreateBackups) { + $convertParams.CreateBackup = $true + } + + $result = Convert-FileEncodingSingle @convertParams + + if ($result) { + $results += $result + + switch ($result.Status) { + 'Converted' { $converted++ } + 'Skipped' { $skipped++ } + 'Error' { $errors++ } + 'Failed' { $errors++ } + } + + # Move backup to specified directory if requested + if ($CreateBackups -and $BackupDirectory -and $result.BackupPath -and (Test-Path $result.BackupPath)) { + $relativePath = Get-RelativePath -From $Path -To $file.FullName + $backupTargetPath = Join-Path $BackupDirectory $relativePath + $backupTargetDir = Split-Path $backupTargetPath -Parent + + if (-not (Test-Path $backupTargetDir)) { + New-Item -Path $backupTargetDir -ItemType Directory -Force | Out-Null + } + + Move-Item -Path $result.BackupPath -Destination $backupTargetPath -Force + $result.BackupPath = $backupTargetPath + } + } + } catch { + Write-Warning "Unexpected error processing $($file.FullName): $_" + $errors++ + } + } + + # Summary report + $summary = @{ + TotalFiles = $uniqueFiles.Count + Converted = $converted + Skipped = $skipped + Errors = $errors + SourceEncoding = $SourceEncoding + TargetEncoding = $TargetEncoding + ProjectPath = $Path + ProjectType = if ($PSCmdlet.ParameterSetName -eq 'Custom') { "Custom ($($CustomExtensions -join ', '))" } else { $ProjectType } + } + + Write-Host "`nConversion Summary:" -ForegroundColor Cyan + Write-Host " Total files processed: $($summary.TotalFiles)" -ForegroundColor White + Write-Host " Successfully converted: $($summary.Converted)" -ForegroundColor Green + Write-Host " Skipped: $($summary.Skipped)" -ForegroundColor Yellow + Write-Host " Errors: $($summary.Errors)" -ForegroundColor Red + Write-Host " Encoding: $($summary.SourceEncoding) → $($summary.TargetEncoding)" -ForegroundColor White + + if ($PassThru) { + [PSCustomObject]@{ + Summary = $summary + Results = $results + } + } +} diff --git a/Public/Convert-ProjectLineEnding.ps1 b/Public/Convert-ProjectLineEnding.ps1 new file mode 100644 index 00000000..f09eaa89 --- /dev/null +++ b/Public/Convert-ProjectLineEnding.ps1 @@ -0,0 +1,275 @@ +function Convert-ProjectLineEnding { + <# + .SYNOPSIS + Converts line endings for all source files in a project directory with comprehensive safety features. + + .DESCRIPTION + Recursively converts line endings for PowerShell, C#, and other source code files in a project directory. + Includes comprehensive safety features: WhatIf support, automatic backups, rollback protection, + and detailed reporting. Can convert between CRLF (Windows), LF (Unix/Linux), and fix mixed line endings. + + .PARAMETER Path + Path to the project directory to process. + + .PARAMETER ProjectType + Type of project to process. Determines which file extensions are included. + Valid values: 'PowerShell', 'CSharp', 'Mixed', 'All', 'Custom' + + .PARAMETER CustomExtensions + Custom file extensions to process when ProjectType is 'Custom'. + Example: @('*.ps1', '*.psm1', '*.cs', '*.vb') + + .PARAMETER TargetLineEnding + Target line ending style. Valid values: 'CRLF', 'LF' + + .PARAMETER ExcludeDirectories + Directory names to exclude from processing (e.g., '.git', 'bin', 'obj'). + + .PARAMETER CreateBackups + Create backup files before conversion for additional safety. + + .PARAMETER BackupDirectory + Directory to store backup files. If not specified, backups are created alongside original files. + + .PARAMETER Force + Convert all files regardless of current line ending type. + + .PARAMETER EnsureFinalNewline + Ensure all files end with a newline character (POSIX compliance). + + .PARAMETER OnlyMissingNewline + Only process files that are missing final newlines, leave others unchanged. + + .PARAMETER PassThru + Return detailed results for each processed file. + + .EXAMPLE + Convert-ProjectLineEnding -Path 'C:\MyProject' -ProjectType PowerShell -TargetLineEnding CRLF -WhatIf + Preview what files would be converted to Windows-style line endings. + + .EXAMPLE + Convert-ProjectLineEnding -Path 'C:\MyProject' -ProjectType Mixed -TargetLineEnding LF -CreateBackups + Convert a mixed project to Unix-style line endings with backups. + + .EXAMPLE + Convert-ProjectLineEnding -Path 'C:\MyProject' -ProjectType All -OnlyMixed -PassThru + Fix only files with mixed line endings and return detailed results. + + .NOTES + This function modifies files in place. Always use -WhatIf first or -CreateBackups for safety. + Line ending types: + - CRLF: Windows style (\\r\\n) + - LF: Unix/Linux style (\\n) + #> + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory)] + [string] $Path, + + [ValidateSet('PowerShell', 'CSharp', 'Mixed', 'All', 'Custom')] + [string] $ProjectType = 'Mixed', + + [string[]] $CustomExtensions, + + [Parameter(Mandatory)] + [ValidateSet('CRLF', 'LF')] + [string] $TargetLineEnding, + + [string[]] $ExcludeDirectories = @('.git', '.vs', 'bin', 'obj', 'packages', 'node_modules', '.vscode'), + + [switch] $CreateBackups, + [string] $BackupDirectory, + [switch] $Force, + [switch] $OnlyMixed, + [switch] $EnsureFinalNewline, + [switch] $OnlyMissingNewline, + [switch] $PassThru + ) + + # Validate path + if (-not (Test-Path -LiteralPath $Path -PathType Container)) { + throw "Project path '$Path' not found or is not a directory" + } + + # Define file extension mappings + $extensionMappings = @{ + 'PowerShell' = @('*.ps1', '*.psm1', '*.psd1', '*.ps1xml') + 'CSharp' = @('*.cs', '*.csx', '*.csproj', '*.sln', '*.config', '*.json', '*.xml', '*.resx') + 'Mixed' = @('*.ps1', '*.psm1', '*.psd1', '*.ps1xml', '*.cs', '*.csx', '*.csproj', '*.sln', '*.config', '*.json', '*.xml') + 'All' = @('*.ps1', '*.psm1', '*.psd1', '*.ps1xml', '*.cs', '*.csx', '*.csproj', '*.sln', '*.config', '*.json', '*.xml', '*.js', '*.ts', '*.py', '*.rb', '*.java', '*.cpp', '*.h', '*.hpp', '*.sql', '*.md', '*.txt', '*.yaml', '*.yml') + } + + # Determine file patterns to process + if ($ProjectType -eq 'Custom' -and $CustomExtensions) { + $filePatterns = $CustomExtensions + } else { + $filePatterns = $extensionMappings[$ProjectType] + } + + Write-Verbose "Processing project type: $ProjectType with patterns: $($filePatterns -join ', ')" # Helper function to detect current line endings and final newline + + # Prepare backup directory if specified + if ($CreateBackups -and $BackupDirectory) { + if (-not (Test-Path -LiteralPath $BackupDirectory)) { + New-Item -Path $BackupDirectory -ItemType Directory -Force | Out-Null + Write-Verbose "Created backup directory: $BackupDirectory" + } + } + + # Collect all files to process + $allFiles = @() + + foreach ($pattern in $filePatterns) { + $params = @{ + Path = $Path + Filter = $pattern + File = $true + Recurse = $true + } + + $files = Get-ChildItem @params | Where-Object { + $file = $_ + $excluded = $false + + foreach ($excludeDir in $ExcludeDirectories) { + if ($file.DirectoryName -like "*\$excludeDir" -or $file.DirectoryName -like "*\$excludeDir\*") { + $excluded = $true + break + } + } + + -not $excluded + } + + $allFiles += $files + } + + # Remove duplicates + $uniqueFiles = $allFiles | Sort-Object FullName | Get-Unique -AsString + + Write-Host "Found $($uniqueFiles.Count) files to process" -ForegroundColor Green + + if ($uniqueFiles.Count -eq 0) { + Write-Warning "No files found matching the specified criteria" + return + } + + # Process files + $results = @() + $converted = 0 + $skipped = 0 + $errors = 0 + + foreach ($file in $uniqueFiles) { + try { + $currentInfo = Get-CurrentLineEnding -FilePath $file.FullName + $currentLineEnding = $currentInfo.LineEnding + $hasFinalNewline = $currentInfo.HasFinalNewline + $relativePath = Get-RelativePath -From $Path -To $file.FullName + + # Determine if file should be processed + $shouldProcess = $false + $skipReason = "" + + if ($currentLineEnding -eq 'Error') { + $skipReason = "Could not detect line endings" + } elseif ($currentLineEnding -eq 'None') { + $skipReason = "Empty file or no line endings" + } elseif ($OnlyMixed -and $currentLineEnding -ne 'Mixed') { + $skipReason = "Not mixed line endings (OnlyMixed specified)" + } elseif ($OnlyMissingNewline -and $hasFinalNewline) { + $skipReason = "Already has final newline (OnlyMissingNewline specified)" + } elseif (-not $Force -and $currentLineEnding -eq $TargetLineEnding -and ($hasFinalNewline -or -not $EnsureFinalNewline)) { + $skipReason = "Already compliant with target settings" + } else { + $shouldProcess = $true + } + + if (-not $shouldProcess) { + $result = @{ + FilePath = $relativePath + FullPath = $file.FullName + Status = 'Skipped' + Reason = $skipReason + CurrentLineEnding = $currentLineEnding + TargetLineEnding = $TargetLineEnding + HasFinalNewline = $hasFinalNewline + } + $results += [PSCustomObject]$result + $skipped++ + Write-Verbose "Skipped $relativePath`: $skipReason" + continue + } + + if ($PSCmdlet.ShouldProcess($relativePath, "Convert line endings from $currentLineEnding to $TargetLineEnding$(if ($EnsureFinalNewline) { ' and ensure final newline' })")) { + $conversionResult = Convert-LineEndingSingle -FilePath $file.FullName -TargetLineEnding $TargetLineEnding -CurrentInfo $currentInfo -CreateBackup $CreateBackups -EnsureFinalNewline $EnsureFinalNewline + + $result = @{ + FilePath = $relativePath + FullPath = $file.FullName + Status = $conversionResult.Status + Reason = $conversionResult.Reason + CurrentLineEnding = $currentLineEnding + TargetLineEnding = $TargetLineEnding + HasFinalNewline = $hasFinalNewline + BackupPath = $conversionResult.BackupPath + } + + # Move backup to specified directory if requested + if ($CreateBackups -and $BackupDirectory -and $conversionResult.BackupPath -and (Test-Path $conversionResult.BackupPath)) { + $backupTargetPath = Join-Path $BackupDirectory $relativePath + $backupTargetDir = Split-Path $backupTargetPath -Parent + + if (-not (Test-Path $backupTargetDir)) { + New-Item -Path $backupTargetDir -ItemType Directory -Force | Out-Null + } + + Move-Item -Path $conversionResult.BackupPath -Destination $backupTargetPath -Force + $result.BackupPath = $backupTargetPath + } + + $results += [PSCustomObject]$result + + switch ($conversionResult.Status) { + 'Converted' { + $converted++ + Write-Verbose "Converted $relativePath from $currentLineEnding to $TargetLineEnding" + } + 'Error' { + $errors++ + Write-Warning "Failed to convert $relativePath`: $($conversionResult.Reason)" + } + default { $skipped++ } + } + } + } catch { + Write-Warning "Unexpected error processing $($file.FullName): $_" + $errors++ + } + } + + # Summary report + $summary = @{ + TotalFiles = $uniqueFiles.Count + Converted = $converted + Skipped = $skipped + Errors = $errors + TargetLineEnding = $TargetLineEnding + ProjectPath = $Path + ProjectType = if ($ProjectType -eq 'Custom') { "Custom ($($CustomExtensions -join ', '))" } else { $ProjectType } + } + + Write-Host "`nLine Ending Conversion Summary:" -ForegroundColor Cyan + Write-Host " Total files processed: $($summary.TotalFiles)" -ForegroundColor White + Write-Host " Successfully converted: $($summary.Converted)" -ForegroundColor Green + Write-Host " Skipped: $($summary.Skipped)" -ForegroundColor Yellow + Write-Host " Errors: $($summary.Errors)" -ForegroundColor Red + Write-Host " Target line ending: $($summary.TargetLineEnding)" -ForegroundColor White + + if ($PassThru) { + [PSCustomObject]@{ + Summary = $summary + Results = $results + } + } +} diff --git a/Public/Get-ProjectConsistency.ps1 b/Public/Get-ProjectConsistency.ps1 new file mode 100644 index 00000000..80a4d474 --- /dev/null +++ b/Public/Get-ProjectConsistency.ps1 @@ -0,0 +1,292 @@ +function Get-ProjectConsistency { + <# + .SYNOPSIS + Provides comprehensive analysis of encoding and line ending consistency across a project. + + .DESCRIPTION + Combines encoding and line ending analysis to provide a complete picture of file consistency + across a project. Identifies issues and provides recommendations for standardization. + This is the main analysis function that should be run before any bulk conversions. + + .PARAMETER Path + Path to the project directory to analyze. + + .PARAMETER ProjectType + Type of project to analyze. Determines which file extensions are included. + Valid values: 'PowerShell', 'CSharp', 'Mixed', 'All', 'Custom' + + .PARAMETER CustomExtensions + Custom file extensions to analyze when ProjectType is 'Custom'. + Example: @('*.ps1', '*.psm1', '*.cs', '*.vb') + + .PARAMETER ExcludeDirectories + Directory names to exclude from analysis (e.g., '.git', 'bin', 'obj'). + + .PARAMETER RecommendedEncoding + The encoding standard you want to achieve. + Default is 'UTF8BOM' for PowerShell projects (PS 5.1 compatibility), 'UTF8' for others. + + .PARAMETER RecommendedLineEnding + The line ending standard you want to achieve. Default is 'CRLF' on Windows, 'LF' on Unix. + + .PARAMETER ShowDetails + Include detailed file-by-file analysis in the output. + + .PARAMETER ExportPath + Export the detailed report to a CSV file at the specified path. + + .EXAMPLE + Get-ProjectConsistencyReport -Path 'C:\MyProject' -ProjectType PowerShell + Analyze consistency in a PowerShell project with UTF8BOM encoding (PS 5.1 compatible). + + .EXAMPLE + Get-ProjectConsistencyReport -Path 'C:\MyProject' -ProjectType Mixed -RecommendedEncoding UTF8BOM -RecommendedLineEnding LF -ShowDetails + Analyze a mixed project with specific recommendations and detailed output. + + .EXAMPLE + Get-ProjectConsistencyReport -Path 'C:\MyProject' -ProjectType CSharp -RecommendedEncoding UTF8 -ExportPath 'C:\Reports\consistency-report.csv' + Analyze a C# project (UTF8 without BOM is fine) with CSV export. + + .NOTES + This function combines the analysis from Get-ProjectEncoding and Get-ProjectLineEnding + to provide a unified view of project file consistency. Use this before running conversion functions. + + Encoding Recommendations: + - PowerShell: UTF8BOM (required for PS 5.1 compatibility with special characters) + - C#: UTF8 (BOM not needed, Visual Studio handles UTF8 correctly) + - Mixed: UTF8BOM (safest for cross-platform PowerShell compatibility) + + PowerShell 5.1 Compatibility: + UTF8 without BOM can cause PowerShell 5.1 to misinterpret files as ASCII, leading to: + - Broken special characters and accented letters + - Module import failures + - Incorrect string processing + UTF8BOM ensures proper encoding detection across all PowerShell versions. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string] $Path, + + [ValidateSet('PowerShell', 'CSharp', 'Mixed', 'All', 'Custom')] + [string] $ProjectType = 'Mixed', + + [string[]] $CustomExtensions, + + [string[]] $ExcludeDirectories = @('.git', '.vs', 'bin', 'obj', 'packages', 'node_modules', '.vscode'), + + [ValidateSet('Ascii', 'BigEndianUnicode', 'Unicode', 'UTF7', 'UTF8', 'UTF8BOM', 'UTF32', 'Default', 'OEM')] + [string] $RecommendedEncoding = $( + if ($ProjectType -eq 'PowerShell') { 'UTF8BOM' } + elseif ($ProjectType -eq 'Mixed') { 'UTF8BOM' } # Default to PowerShell-safe for mixed projects + else { 'UTF8' } + ), + + [ValidateSet('CRLF', 'LF')] + [string] $RecommendedLineEnding = $(if ($IsWindows) { 'CRLF' } else { 'LF' }), + + [switch] $ShowDetails, + [string] $ExportPath + ) + + Write-Host "🔍 Analyzing project consistency..." -ForegroundColor Cyan + Write-Host "Project: $Path" -ForegroundColor White + Write-Host "Type: $ProjectType" -ForegroundColor White + Write-Host "Target encoding: $RecommendedEncoding" -ForegroundColor White + Write-Host "Target line ending: $RecommendedLineEnding" -ForegroundColor White + + # Get encoding analysis + Write-Host "`n📝 Analyzing file encodings..." -ForegroundColor Yellow + $encodingParams = @{ + Path = $Path + ProjectType = $ProjectType + ExcludeDirectories = $ExcludeDirectories + ShowFiles = $true + } + if ($ProjectType -eq 'Custom' -and $CustomExtensions) { + $encodingParams.CustomExtensions = $CustomExtensions + } + + $encodingReport = Get-ProjectEncoding @encodingParams + + # Get line ending analysis + Write-Host "`n📏 Analyzing line endings..." -ForegroundColor Yellow + $lineEndingParams = @{ + Path = $Path + ProjectType = $ProjectType + ExcludeDirectories = $ExcludeDirectories + ShowFiles = $true + CheckMixed = $true + } + if ($ProjectType -eq 'Custom' -and $CustomExtensions) { + $lineEndingParams.CustomExtensions = $CustomExtensions + } + + $lineEndingReport = Get-ProjectLineEnding @lineEndingParams + + # Combine analysis + Write-Host "`n🔄 Combining analysis..." -ForegroundColor Yellow + + # Create comprehensive file details + $allFiles = @() + foreach ($encFile in $encodingReport.Files) { + $leFile = $lineEndingReport.Files | Where-Object { $_.FullPath -eq $encFile.FullPath } + + if ($leFile) { $needsEncodingConversion = $encFile.Encoding -ne $RecommendedEncoding + $needsLineEndingConversion = $leFile.LineEnding -ne $RecommendedLineEnding -and $leFile.LineEnding -ne 'None' + $hasMixedLineEndings = $leFile.LineEnding -eq 'Mixed' + $missingFinalNewline = -not $leFile.HasFinalNewline -and $encFile.Size -gt 0 -and $encFile.Extension -in @('.ps1', '.psm1', '.psd1', '.cs', '.js', '.py', '.rb', '.java', '.cpp', '.h', '.hpp', '.sql', '.md', '.txt', '.yaml', '.yml') + + $fileDetail = [PSCustomObject]@{ + RelativePath = $encFile.RelativePath + FullPath = $encFile.FullPath + Extension = $encFile.Extension + CurrentEncoding = $encFile.Encoding + CurrentLineEnding = $leFile.LineEnding + RecommendedEncoding = $RecommendedEncoding + RecommendedLineEnding = $RecommendedLineEnding + NeedsEncodingConversion = $needsEncodingConversion + NeedsLineEndingConversion = $needsLineEndingConversion + HasMixedLineEndings = $hasMixedLineEndings + MissingFinalNewline = $missingFinalNewline + HasIssues = $needsEncodingConversion -or $needsLineEndingConversion -or $hasMixedLineEndings -or $missingFinalNewline + Size = $encFile.Size + LastModified = $encFile.LastModified + Directory = $encFile.Directory + } + + $allFiles += $fileDetail + } + } + + # Calculate comprehensive statistics + $totalFiles = $allFiles.Count + $filesNeedingEncodingConversion = ($allFiles | Where-Object { $_.NeedsEncodingConversion }).Count + $filesNeedingLineEndingConversion = ($allFiles | Where-Object { $_.NeedsLineEndingConversion }).Count + $filesWithMixedLineEndings = ($allFiles | Where-Object { $_.HasMixedLineEndings }).Count + $filesMissingFinalNewline = ($allFiles | Where-Object { $_.MissingFinalNewline }).Count + $filesWithIssues = ($allFiles | Where-Object { $_.HasIssues }).Count + $filesCompliant = $totalFiles - $filesWithIssues + + # Identify problematic extensions + $extensionIssues = @{} + foreach ($file in ($allFiles | Where-Object { $_.HasIssues })) { + if (-not $extensionIssues.ContainsKey($file.Extension)) { + $extensionIssues[$file.Extension] = @{ + Total = 0 + EncodingIssues = 0 + LineEndingIssues = 0 + MixedLineEndings = 0 + } + } + $extensionIssues[$file.Extension].Total++ + if ($file.NeedsEncodingConversion) { $extensionIssues[$file.Extension].EncodingIssues++ } + if ($file.NeedsLineEndingConversion) { $extensionIssues[$file.Extension].LineEndingIssues++ } + if ($file.HasMixedLineEndings) { $extensionIssues[$file.Extension].MixedLineEndings++ } + } + + # Create comprehensive summary + $summary = [PSCustomObject]@{ + ProjectPath = $Path + ProjectType = $ProjectType + AnalysisDate = Get-Date + + # File counts + TotalFiles = $totalFiles + FilesCompliant = $filesCompliant + FilesWithIssues = $filesWithIssues + CompliancePercentage = [math]::Round(($filesCompliant / $totalFiles) * 100, 1) + + # Encoding statistics + CurrentEncodingDistribution = $encodingReport.Summary.EncodingDistribution + FilesNeedingEncodingConversion = $filesNeedingEncodingConversion + RecommendedEncoding = $RecommendedEncoding + + # Line ending statistics + CurrentLineEndingDistribution = $lineEndingReport.Summary.LineEndingDistribution + FilesNeedingLineEndingConversion = $filesNeedingLineEndingConversion + FilesWithMixedLineEndings = $filesWithMixedLineEndings + FilesMissingFinalNewline = $filesMissingFinalNewline + RecommendedLineEnding = $RecommendedLineEnding + + # Issues by extension + ExtensionIssues = $extensionIssues + } + + # Display comprehensive summary + Write-Host "`n📊 Project Consistency Summary:" -ForegroundColor Cyan + Write-Host " Total files analyzed: $totalFiles" -ForegroundColor White + Write-Host " Files compliant with standards: $filesCompliant ($($summary.CompliancePercentage)%)" -ForegroundColor $(if ($summary.CompliancePercentage -ge 90) { 'Green' } elseif ($summary.CompliancePercentage -ge 70) { 'Yellow' } else { 'Red' }) + Write-Host " Files needing attention: $filesWithIssues" -ForegroundColor $(if ($filesWithIssues -eq 0) { 'Green' } else { 'Red' }) + + Write-Host "`n📝 Encoding Issues:" -ForegroundColor Cyan + Write-Host " Files needing encoding conversion: $filesNeedingEncodingConversion" -ForegroundColor $(if ($filesNeedingEncodingConversion -eq 0) { 'Green' } else { 'Yellow' }) + Write-Host " Target encoding: $RecommendedEncoding" -ForegroundColor White + + Write-Host "`n📏 Line Ending Issues:" -ForegroundColor Cyan + Write-Host " Files needing line ending conversion: $filesNeedingLineEndingConversion" -ForegroundColor $(if ($filesNeedingLineEndingConversion -eq 0) { 'Green' } else { 'Yellow' }) + Write-Host " Files with mixed line endings: $filesWithMixedLineEndings" -ForegroundColor $(if ($filesWithMixedLineEndings -eq 0) { 'Green' } else { 'Red' }) + Write-Host " Files missing final newline: $filesMissingFinalNewline" -ForegroundColor $(if ($filesMissingFinalNewline -eq 0) { 'Green' } else { 'Yellow' }) + Write-Host " Target line ending: $RecommendedLineEnding" -ForegroundColor White + + if ($extensionIssues.Count -gt 0) { + Write-Host "`n⚠️ Extensions with Issues:" -ForegroundColor Yellow + foreach ($ext in ($extensionIssues.GetEnumerator() | Sort-Object { $_.Value.Total } -Descending)) { + Write-Host " ${ext.Key}: $($ext.Value.Total) files" -ForegroundColor White + if ($ext.Value.EncodingIssues -gt 0) { + Write-Host " - Encoding issues: $($ext.Value.EncodingIssues)" -ForegroundColor Yellow + } + if ($ext.Value.LineEndingIssues -gt 0) { + Write-Host " - Line ending issues: $($ext.Value.LineEndingIssues)" -ForegroundColor Yellow + } + if ($ext.Value.MixedLineEndings -gt 0) { + Write-Host " - Mixed line endings: $($ext.Value.MixedLineEndings)" -ForegroundColor Red + } + } + } + + # Recommendations + Write-Host "`n💡 Recommendations:" -ForegroundColor Green + if ($filesWithIssues -eq 0) { + Write-Host " ✅ Your project is fully compliant! No action needed." -ForegroundColor Green + } else { + if ($filesWithMixedLineEndings -gt 0) { + Write-Host " 🔴 Priority 1: Fix mixed line endings first" -ForegroundColor Red + Write-Host " Convert-ProjectLineEnding -Path '$Path' -ProjectType $ProjectType -TargetLineEnding $RecommendedLineEnding -OnlyMixed -CreateBackups" -ForegroundColor Gray + } + if ($filesNeedingEncodingConversion -gt 0) { + Write-Host " 🟡 Priority 2: Standardize encoding" -ForegroundColor Yellow + Write-Host " Convert-ProjectEncoding -Path '$Path' -ProjectType $ProjectType -TargetEncoding $RecommendedEncoding -CreateBackups" -ForegroundColor Gray + } + if ($filesNeedingLineEndingConversion -gt 0) { + Write-Host " 🟡 Priority 3: Standardize line endings" -ForegroundColor Yellow + Write-Host " Convert-ProjectLineEnding -Path '$Path' -ProjectType $ProjectType -TargetLineEnding $RecommendedLineEnding -CreateBackups" -ForegroundColor Gray + } + if ($filesMissingFinalNewline -gt 0) { + Write-Host " 🟡 Priority 4: Add missing final newlines" -ForegroundColor Yellow + Write-Host " Convert-ProjectLineEnding -Path '$Path' -ProjectType $ProjectType -TargetLineEnding $RecommendedLineEnding -EnsureFinalNewline -OnlyMissingNewline -CreateBackups" -ForegroundColor Gray + } + Write-Host " 💾 Always use -WhatIf first and -CreateBackups for safety!" -ForegroundColor Cyan + } + + # Prepare return object + $report = [PSCustomObject]@{ + Summary = $summary + EncodingReport = $encodingReport + LineEndingReport = $lineEndingReport + Files = if ($ShowDetails) { $allFiles } else { $null } + ProblematicFiles = $allFiles | Where-Object { $_.HasIssues } + } + + # Export to CSV if requested + if ($ExportPath) { + try { + $allFiles | Export-Csv -Path $ExportPath -NoTypeInformation -Encoding UTF8 + Write-Host "`nDetailed report exported to: $ExportPath" -ForegroundColor Green + } catch { + Write-Warning "Failed to export report to $ExportPath`: $_" + } + } + + return $report +} diff --git a/Public/Get-ProjectEncoding.ps1 b/Public/Get-ProjectEncoding.ps1 new file mode 100644 index 00000000..67f22198 --- /dev/null +++ b/Public/Get-ProjectEncoding.ps1 @@ -0,0 +1,257 @@ +function Get-ProjectEncoding { + <# + .SYNOPSIS + Analyzes encoding consistency across all files in a project directory. + + .DESCRIPTION + Scans all relevant files in a project directory and provides a comprehensive report on file encodings. + Identifies inconsistencies, potential issues, and provides recommendations for standardization. + Useful for auditing projects before performing encoding conversions. + + .PARAMETER Path + Path to the project directory to analyze. + + .PARAMETER ProjectType + Type of project to analyze. Determines which file extensions are included. + Valid values: 'PowerShell', 'CSharp', 'Mixed', 'All', 'Custom' + + .PARAMETER CustomExtensions + Custom file extensions to analyze when ProjectType is 'Custom'. + Example: @('*.ps1', '*.psm1', '*.cs', '*.vb') + + .PARAMETER ExcludeDirectories + Directory names to exclude from analysis (e.g., '.git', 'bin', 'obj'). + + .PARAMETER GroupByEncoding + Group results by encoding type for easier analysis. + + .PARAMETER ShowFiles + Include individual file details in the report. + + .PARAMETER ExportPath + Export the detailed report to a CSV file at the specified path. + + .EXAMPLE + Get-ProjectEncoding -Path 'C:\MyProject' -ProjectType PowerShell + Analyze encoding consistency in a PowerShell project. + + .EXAMPLE + Get-ProjectEncoding -Path 'C:\MyProject' -ProjectType Mixed -GroupByEncoding -ShowFiles + Get detailed encoding report grouped by encoding type with individual file listings. + + .EXAMPLE + Get-ProjectEncoding -Path 'C:\MyProject' -ProjectType All -ExportPath 'C:\Reports\encoding-report.csv' + Analyze all file types and export detailed report to CSV. + + .NOTES + This function is read-only and does not modify any files. Use Convert-ProjectEncoding to standardize encodings. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string] $Path, + + [ValidateSet('PowerShell', 'CSharp', 'Mixed', 'All', 'Custom')] + [string] $ProjectType = 'Mixed', + + [string[]] $CustomExtensions, + + [string[]] $ExcludeDirectories = @('.git', '.vs', 'bin', 'obj', 'packages', 'node_modules', '.vscode'), + + [switch] $GroupByEncoding, + [switch] $ShowFiles, + [string] $ExportPath + ) + + # Validate path + if (-not (Test-Path -LiteralPath $Path -PathType Container)) { + throw "Project path '$Path' not found or is not a directory" + } + + # Define file extension mappings + $extensionMappings = @{ + 'PowerShell' = @('*.ps1', '*.psm1', '*.psd1', '*.ps1xml') + 'CSharp' = @('*.cs', '*.csx', '*.csproj', '*.sln', '*.config', '*.json', '*.xml', '*.resx') + 'Mixed' = @('*.ps1', '*.psm1', '*.psd1', '*.ps1xml', '*.cs', '*.csx', '*.csproj', '*.sln', '*.config', '*.json', '*.xml') + 'All' = @('*.ps1', '*.psm1', '*.psd1', '*.ps1xml', '*.cs', '*.csx', '*.csproj', '*.sln', '*.config', '*.json', '*.xml', '*.js', '*.ts', '*.py', '*.rb', '*.java', '*.cpp', '*.h', '*.hpp', '*.sql', '*.md', '*.txt', '*.yaml', '*.yml') + } + + # Determine file patterns to analyze + if ($ProjectType -eq 'Custom' -and $CustomExtensions) { + $filePatterns = $CustomExtensions + } else { + $filePatterns = $extensionMappings[$ProjectType] + } + + Write-Host "Analyzing project encoding..." -ForegroundColor Cyan + Write-Verbose "Project type: $ProjectType with patterns: $($filePatterns -join ', ')" + + # Collect all files to analyze + $allFiles = @() + + foreach ($pattern in $filePatterns) { + $params = @{ + Path = $Path + Filter = $pattern + File = $true + Recurse = $true + } + + $files = Get-ChildItem @params | Where-Object { + $file = $_ + $excluded = $false + + foreach ($excludeDir in $ExcludeDirectories) { + if ($file.DirectoryName -like "*\$excludeDir" -or $file.DirectoryName -like "*\$excludeDir\*") { + $excluded = $true + break + } + } + + -not $excluded + } + + $allFiles += $files + } + + # Remove duplicates + $uniqueFiles = $allFiles | Sort-Object FullName | Get-Unique -AsString + + if ($uniqueFiles.Count -eq 0) { + Write-Warning "No files found matching the specified criteria" + return + } + + Write-Host "Analyzing $($uniqueFiles.Count) files..." -ForegroundColor Green + + # Analyze each file + $fileDetails = @() + $encodingStats = @{} + $extensionStats = @{} + + foreach ($file in $uniqueFiles) { + try { + $encodingInfo = Get-FileEncoding -Path $file.FullName -AsObject + $extension = $file.Extension.ToLower() + $relativePath = Get-RelativePath -From $Path -To $file.FullName + + $fileDetail = [PSCustomObject]@{ + RelativePath = $relativePath + FullPath = $file.FullName + Extension = $extension + Encoding = $encodingInfo.EncodingName + Size = $file.Length + LastModified = $file.LastWriteTime + Directory = $file.DirectoryName + } + + $fileDetails += $fileDetail + + # Update encoding statistics + if (-not $encodingStats.ContainsKey($encodingInfo.EncodingName)) { + $encodingStats[$encodingInfo.EncodingName] = 0 + } + $encodingStats[$encodingInfo.EncodingName]++ + + # Update extension statistics + if (-not $extensionStats.ContainsKey($extension)) { + $extensionStats[$extension] = @{} + } + if (-not $extensionStats[$extension].ContainsKey($encodingInfo.EncodingName)) { + $extensionStats[$extension][$encodingInfo.EncodingName] = 0 + } + $extensionStats[$extension][$encodingInfo.EncodingName]++ + + } catch { + Write-Warning "Failed to analyze $($file.FullName): $_" + } + } + + # Generate summary statistics + $totalFiles = $fileDetails.Count + $uniqueEncodings = $encodingStats.Keys | Sort-Object + $mostCommonEncoding = ($encodingStats.GetEnumerator() | Sort-Object Value -Descending | Select-Object -First 1).Key + $inconsistentExtensions = @() + + # Find extensions with mixed encodings + foreach ($ext in $extensionStats.Keys) { + if ($extensionStats[$ext].Count -gt 1) { + $inconsistentExtensions += $ext + } + } + + # Create summary report + $summary = [PSCustomObject]@{ + ProjectPath = $Path + ProjectType = $ProjectType + TotalFiles = $totalFiles + UniqueEncodings = $uniqueEncodings + EncodingCount = $uniqueEncodings.Count + MostCommonEncoding = $mostCommonEncoding + InconsistentExtensions = $inconsistentExtensions + EncodingDistribution = $encodingStats + ExtensionEncodingMap = $extensionStats + AnalysisDate = Get-Date + } + + # Display summary + Write-Host "`nEncoding Analysis Summary:" -ForegroundColor Cyan + Write-Host " Total files analyzed: $totalFiles" -ForegroundColor White + Write-Host " Unique encodings found: $($uniqueEncodings.Count)" -ForegroundColor White + + if ($totalFiles -gt 0 -and $mostCommonEncoding) { + Write-Host " Most common encoding: $mostCommonEncoding ($($encodingStats[$mostCommonEncoding]) files)" -ForegroundColor Green + } elseif ($totalFiles -eq 0) { + Write-Host " No files found for analysis" -ForegroundColor Yellow + return $result + } else { + Write-Host " No encoding information available" -ForegroundColor Yellow + } + + if ($inconsistentExtensions.Count -gt 0) { + Write-Host " ⚠️ Extensions with mixed encodings: $($inconsistentExtensions -join ', ')" -ForegroundColor Yellow + } else { + Write-Host " ✅ All file extensions have consistent encodings" -ForegroundColor Green + } + + Write-Host "`nEncoding Distribution:" -ForegroundColor Cyan + foreach ($encoding in ($encodingStats.GetEnumerator() | Sort-Object Value -Descending)) { + $percentage = [math]::Round(($encoding.Value / $totalFiles) * 100, 1) + Write-Host " $($encoding.Key): $($encoding.Value) files ($percentage%)" -ForegroundColor White + } + + if ($inconsistentExtensions.Count -gt 0) { + Write-Host "`nExtensions with Mixed Encodings:" -ForegroundColor Yellow + foreach ($ext in $inconsistentExtensions) { + Write-Host " ${ext}:" -ForegroundColor Yellow + foreach ($encoding in ($extensionStats[$ext].GetEnumerator() | Sort-Object Value -Descending)) { + Write-Host " $($encoding.Key): $($encoding.Value) files" -ForegroundColor White + } + } + } + + # Prepare return object + $report = [PSCustomObject]@{ + Summary = $summary + Files = if ($ShowFiles) { $fileDetails } else { $null } + GroupedByEncoding = if ($GroupByEncoding) { + $grouped = @{} + foreach ($encoding in $uniqueEncodings) { + $grouped[$encoding] = $fileDetails | Where-Object { $_.Encoding -eq $encoding } + } + $grouped + } else { $null } + } + + # Export to CSV if requested + if ($ExportPath) { + try { + $fileDetails | Export-Csv -Path $ExportPath -NoTypeInformation -Encoding UTF8 + Write-Host "`nDetailed report exported to: $ExportPath" -ForegroundColor Green + } catch { + Write-Warning "Failed to export report to $ExportPath`: $_" + } + } + + return $report +} diff --git a/Public/Get-ProjectLineEnding.ps1 b/Public/Get-ProjectLineEnding.ps1 new file mode 100644 index 00000000..1b6a5178 --- /dev/null +++ b/Public/Get-ProjectLineEnding.ps1 @@ -0,0 +1,315 @@ +function Get-ProjectLineEnding { + <# + .SYNOPSIS + Analyzes line ending consistency across all files in a project directory. + + .DESCRIPTION + Scans all relevant files in a project directory and provides a comprehensive report on line endings. + Identifies inconsistencies between CRLF (Windows), LF (Unix/Linux), and mixed line endings. + Helps ensure consistency across development environments and prevent Git issues. + + .PARAMETER Path + Path to the project directory to analyze. + + .PARAMETER ProjectType + Type of project to analyze. Determines which file extensions are included. + Valid values: 'PowerShell', 'CSharp', 'Mixed', 'All', 'Custom' + + .PARAMETER CustomExtensions + Custom file extensions to analyze when ProjectType is 'Custom'. + Example: @('*.ps1', '*.psm1', '*.cs', '*.vb') + + .PARAMETER ExcludeDirectories + Directory names to exclude from analysis (e.g., '.git', 'bin', 'obj'). + + .PARAMETER GroupByLineEnding + Group results by line ending type for easier analysis. + + .PARAMETER ShowFiles + Include individual file details in the report. + + .PARAMETER CheckMixed + Additionally check for files with mixed line endings (both CRLF and LF in same file). + + .PARAMETER ExportPath + Export the detailed report to a CSV file at the specified path. + + .EXAMPLE + Get-ProjectLineEnding -Path 'C:\MyProject' -ProjectType PowerShell + Analyze line ending consistency in a PowerShell project. + + .EXAMPLE + Get-ProjectLineEnding -Path 'C:\MyProject' -ProjectType Mixed -CheckMixed -ShowFiles + Get detailed line ending report including mixed line ending detection. + + .EXAMPLE + Get-ProjectLineEnding -Path 'C:\MyProject' -ProjectType All -ExportPath 'C:\Reports\lineending-report.csv' + Analyze all file types and export detailed report to CSV. + + .NOTES + Line ending types: + - CRLF: Windows style (\\r\\n) + - LF: Unix/Linux style (\\n) + - CR: Classic Mac style (\\r) - rarely used + - Mixed: File contains multiple line ending types + - None: Empty file or single line without line ending + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string] $Path, + + [ValidateSet('PowerShell', 'CSharp', 'Mixed', 'All', 'Custom')] + [string] $ProjectType = 'Mixed', + + [string[]] $CustomExtensions, + + [string[]] $ExcludeDirectories = @('.git', '.vs', 'bin', 'obj', 'packages', 'node_modules', '.vscode'), + + [switch] $GroupByLineEnding, + [switch] $ShowFiles, + [switch] $CheckMixed, + [string] $ExportPath + ) + + # Validate path + if (-not (Test-Path -LiteralPath $Path -PathType Container)) { + throw "Project path '$Path' not found or is not a directory" + } + + # Define file extension mappings + $extensionMappings = @{ + 'PowerShell' = @('*.ps1', '*.psm1', '*.psd1', '*.ps1xml') + 'CSharp' = @('*.cs', '*.csx', '*.csproj', '*.sln', '*.config', '*.json', '*.xml', '*.resx') + 'Mixed' = @('*.ps1', '*.psm1', '*.psd1', '*.ps1xml', '*.cs', '*.csx', '*.csproj', '*.sln', '*.config', '*.json', '*.xml') + 'All' = @('*.ps1', '*.psm1', '*.psd1', '*.ps1xml', '*.cs', '*.csx', '*.csproj', '*.sln', '*.config', '*.json', '*.xml', '*.js', '*.ts', '*.py', '*.rb', '*.java', '*.cpp', '*.h', '*.hpp', '*.sql', '*.md', '*.txt', '*.yaml', '*.yml') + } + + # Determine file patterns to analyze + if ($ProjectType -eq 'Custom' -and $CustomExtensions) { + $filePatterns = $CustomExtensions + } else { + $filePatterns = $extensionMappings[$ProjectType] + } + + Write-Host "Analyzing project line endings..." -ForegroundColor Cyan + Write-Verbose "Project type: $ProjectType with patterns: $($filePatterns -join ', ')" # Helper function to detect line endings and final newline + + # Collect all files to analyze + $allFiles = @() + + foreach ($pattern in $filePatterns) { + $params = @{ + Path = $Path + Filter = $pattern + File = $true + Recurse = $true + } + + $files = Get-ChildItem @params | Where-Object { + $file = $_ + $excluded = $false + + foreach ($excludeDir in $ExcludeDirectories) { + if ($file.DirectoryName -like "*\$excludeDir" -or $file.DirectoryName -like "*\$excludeDir\*") { + $excluded = $true + break + } + } + + -not $excluded + } + + $allFiles += $files + } + + # Remove duplicates + $uniqueFiles = $allFiles | Sort-Object FullName | Get-Unique -AsString + + if ($uniqueFiles.Count -eq 0) { + Write-Warning "No files found matching the specified criteria" + return + } + + Write-Host "Analyzing $($uniqueFiles.Count) files..." -ForegroundColor Green + + # Analyze each file + $fileDetails = @() + $lineEndingStats = @{} + $extensionStats = @{} + $problemFiles = @() + $filesWithoutFinalNewline = @() + + foreach ($file in $uniqueFiles) { + try { + $lineEndingInfo = Get-LineEndingType -FilePath $file.FullName + $lineEndingType = $lineEndingInfo.LineEnding + $hasFinalNewline = $lineEndingInfo.HasFinalNewline + $extension = $file.Extension.ToLower() + $relativePath = Get-RelativePath -From $Path -To $file.FullName + + $fileDetail = [PSCustomObject]@{ + RelativePath = $relativePath + FullPath = $file.FullName + Extension = $extension + LineEnding = $lineEndingType + HasFinalNewline = $hasFinalNewline + Size = $file.Length + LastModified = $file.LastWriteTime + Directory = $file.DirectoryName + } + + $fileDetails += $fileDetail + + # Track problem files + if ($lineEndingType -eq 'Mixed' -or ($CheckMixed -and $lineEndingType -eq 'Mixed')) { + $problemFiles += $fileDetail + } + + # Track files without final newlines (excluding empty files and certain types) + if (-not $hasFinalNewline -and $file.Length -gt 0 -and $extension -in @('.ps1', '.psm1', '.psd1', '.cs', '.js', '.py', '.rb', '.java', '.cpp', '.h', '.hpp', '.sql', '.md', '.txt', '.yaml', '.yml')) { + $filesWithoutFinalNewline += $fileDetail + } + + # Update line ending statistics + if (-not $lineEndingStats.ContainsKey($lineEndingType)) { + $lineEndingStats[$lineEndingType] = 0 + } + $lineEndingStats[$lineEndingType]++ + + # Update extension statistics + if (-not $extensionStats.ContainsKey($extension)) { + $extensionStats[$extension] = @{} + } + if (-not $extensionStats[$extension].ContainsKey($lineEndingType)) { + $extensionStats[$extension][$lineEndingType] = 0 + } + $extensionStats[$extension][$lineEndingType]++ + + } catch { + Write-Warning "Failed to analyze $($file.FullName): $_" + } + } + + # Generate summary statistics + $totalFiles = $fileDetails.Count + $uniqueLineEndings = $lineEndingStats.Keys | Sort-Object + $mostCommonLineEnding = ($lineEndingStats.GetEnumerator() | Sort-Object Value -Descending | Select-Object -First 1).Key + $inconsistentExtensions = @() + + # Find extensions with mixed line endings + foreach ($ext in $extensionStats.Keys) { + if ($extensionStats[$ext].Count -gt 1) { + $inconsistentExtensions += $ext + } + } + + # Create summary report + $summary = [PSCustomObject]@{ + ProjectPath = $Path + ProjectType = $ProjectType + TotalFiles = $totalFiles + UniqueLineEndings = $uniqueLineEndings + LineEndingCount = $uniqueLineEndings.Count + MostCommonLineEnding = $mostCommonLineEnding + InconsistentExtensions = $inconsistentExtensions + ProblemFiles = $problemFiles.Count + FilesWithoutFinalNewline = $filesWithoutFinalNewline.Count + LineEndingDistribution = $lineEndingStats + ExtensionLineEndingMap = $extensionStats + AnalysisDate = Get-Date + } + + # Display summary + Write-Host "`nLine Ending Analysis Summary:" -ForegroundColor Cyan + Write-Host " Total files analyzed: $totalFiles" -ForegroundColor White + Write-Host " Unique line endings found: $($uniqueLineEndings.Count)" -ForegroundColor White + Write-Host " Most common line ending: $mostCommonLineEnding ($($lineEndingStats[$mostCommonLineEnding]) files)" -ForegroundColor Green + + if ($problemFiles.Count -gt 0) { + Write-Host " ⚠️ Files with mixed line endings: $($problemFiles.Count)" -ForegroundColor Red + } + + if ($filesWithoutFinalNewline.Count -gt 0) { + Write-Host " ⚠️ Files without final newline: $($filesWithoutFinalNewline.Count)" -ForegroundColor Yellow + } else { + Write-Host " ✅ All files end with proper newlines" -ForegroundColor Green + } + + if ($inconsistentExtensions.Count -gt 0) { + Write-Host " ⚠️ Extensions with mixed line endings: $($inconsistentExtensions -join ', ')" -ForegroundColor Yellow + } else { + Write-Host " ✅ All file extensions have consistent line endings" -ForegroundColor Green + } + + Write-Host "`nLine Ending Distribution:" -ForegroundColor Cyan + foreach ($lineEnding in ($lineEndingStats.GetEnumerator() | Sort-Object Value -Descending)) { + $percentage = [math]::Round(($lineEnding.Value / $totalFiles) * 100, 1) + $color = switch ($lineEnding.Key) { + 'CRLF' { 'Green' } + 'LF' { 'Green' } + 'Mixed' { 'Red' } + 'CR' { 'Yellow' } + 'None' { 'Gray' } + 'Error' { 'Red' } + default { 'White' } + } + Write-Host " $($lineEnding.Key): $($lineEnding.Value) files ($percentage%)" -ForegroundColor $color + } + + if ($problemFiles.Count -gt 0) { + Write-Host "`nFiles with Mixed Line Endings:" -ForegroundColor Red + foreach ($problemFile in $problemFiles | Select-Object -First 10) { + Write-Host " $($problemFile.RelativePath)" -ForegroundColor Yellow + } + if ($problemFiles.Count -gt 10) { + Write-Host " ... and $($problemFiles.Count - 10) more files" -ForegroundColor Yellow + } + } + + if ($filesWithoutFinalNewline.Count -gt 0) { + Write-Host "`nFiles Missing Final Newline:" -ForegroundColor Yellow + foreach ($missingFile in $filesWithoutFinalNewline | Select-Object -First 10) { + Write-Host " $($missingFile.RelativePath)" -ForegroundColor Yellow + } + if ($filesWithoutFinalNewline.Count -gt 10) { + Write-Host " ... and $($filesWithoutFinalNewline.Count - 10) more files" -ForegroundColor Yellow + } + } + + if ($inconsistentExtensions.Count -gt 0) { + Write-Host "`nExtensions with Mixed Line Endings:" -ForegroundColor Yellow + foreach ($ext in $inconsistentExtensions) { + Write-Host " ${ext}:" -ForegroundColor Yellow + foreach ($lineEnding in ($extensionStats[$ext].GetEnumerator() | Sort-Object Value -Descending)) { + Write-Host " $($lineEnding.Key): $($lineEnding.Value) files" -ForegroundColor White + } + } + } + + # Prepare return object + $report = [PSCustomObject]@{ + Summary = $summary + Files = if ($ShowFiles) { $fileDetails } else { $null } + ProblemFiles = $problemFiles + GroupedByLineEnding = if ($GroupByLineEnding) { + $grouped = @{} + foreach ($lineEnding in $uniqueLineEndings) { + $grouped[$lineEnding] = $fileDetails | Where-Object { $_.LineEnding -eq $lineEnding } + } + $grouped + } else { $null } + } + + # Export to CSV if requested + if ($ExportPath) { + try { + $fileDetails | Export-Csv -Path $ExportPath -NoTypeInformation -Encoding UTF8 + Write-Host "`nDetailed report exported to: $ExportPath" -ForegroundColor Green + } catch { + Write-Warning "Failed to export report to $ExportPath`: $_" + } + } + + return $report +} diff --git a/Public/Test-Basic.ps1 b/Public/Test-Basic.ps1 index f5b1369d..4f0d3c52 100644 --- a/Public/Test-Basic.ps1 +++ b/Public/Test-Basic.ps1 @@ -5,6 +5,6 @@ function Test-BasicModule { [string] $Type ) if ($Type -contains 'Encoding') { - Get-ChildItem -LiteralPath $Path -Recurse -Filter '*.ps1' | Get-Encoding + Get-ChildItem -LiteralPath $Path -Recurse -Filter '*.ps1' | Get-FileEncoding } } \ No newline at end of file diff --git a/Tests/Convert-FileEncoding.Tests.ps1 b/Tests/Convert-FileEncoding.Tests.ps1 new file mode 100644 index 00000000..78f44715 --- /dev/null +++ b/Tests/Convert-FileEncoding.Tests.ps1 @@ -0,0 +1,129 @@ +Describe 'Convert-ProjectEncoding' { + BeforeAll { + if ($IsWindows) { + $TempDir = $env:TEMP + } else { + $TempDir = '/tmp' + } + if (-not (Get-Module -ListAvailable -Name 'PSPublishModule')) { + $ModuleToLoad = Join-Path -Path $PSScriptRoot -ChildPath '..' -AdditionalChildPath 'PSPublishModule.psd1' + } else { + $ModuleToLoad = 'PSPublishModule' + } + } + + It 'Returns encoding name by default' { + $f = [System.IO.Path]::GetTempFileName() + # Use UTF8 content with non-ASCII characters to ensure UTF8 detection + [System.IO.File]::WriteAllText($f, 'tëst', [System.Text.UTF8Encoding]::new($false)) + + # Import module with PassThru to access private functions + $Module = Import-Module $ModuleToLoad -Force -PassThru + $enc = & $Module Get-FileEncoding -Path $f + $enc | Should -Be 'UTF8' + Remove-Item $f -Force + } + + It 'Returns ASCII for pure ASCII content' { + $f = [System.IO.Path]::GetTempFileName() + # Pure ASCII content should be detected as ASCII + [System.IO.File]::WriteAllText($f, 'test', [System.Text.UTF8Encoding]::new($false)) + + # Import module with PassThru to access private functions + $Module = Import-Module $ModuleToLoad -Force -PassThru + $enc = & $Module Get-FileEncoding -Path $f + $enc | Should -Be 'ASCII' + Remove-Item $f -Force + } + + It 'Converts files using Convert-ProjectEncoding' { + # Create a temporary directory with test files + $TestDir = Join-Path $TempDir 'encoding-test' + if (Test-Path $TestDir) { Remove-Item $TestDir -Recurse -Force } + New-Item -Path $TestDir -ItemType Directory | Out-Null + + try { + # Create test files with different encodings + $File1 = Join-Path $TestDir 'test1.ps1' + $File2 = Join-Path $TestDir 'test2.ps1' + + [System.IO.File]::WriteAllText($File1, 'Write-Host "Hello"', [System.Text.UTF8Encoding]::new($true)) # UTF8BOM + [System.IO.File]::WriteAllText($File2, 'Write-Host "World"', [System.Text.UTF8Encoding]::new($false)) # UTF8 + + # Import module with PassThru to access private functions for verification + $Module = Import-Module $ModuleToLoad -Force -PassThru + + # Verify initial encodings + $enc1Before = & $Module Get-FileEncoding -Path $File1 + $enc2Before = & $Module Get-FileEncoding -Path $File2 + $enc1Before | Should -Be 'UTF8BOM' + $enc2Before | Should -Be 'ASCII' # Pure ASCII content + + # Convert using the public function + Convert-ProjectEncoding -Path $TestDir -ProjectType PowerShell -TargetEncoding UTF8BOM -Force -WhatIf:$false + + # Verify conversions + $enc1After = & $Module Get-FileEncoding -Path $File1 + $enc2After = & $Module Get-FileEncoding -Path $File2 + $enc1After | Should -Be 'UTF8BOM' # Should remain UTF8BOM + $enc2After | Should -Be 'UTF8BOM' # Should be converted to UTF8BOM + + } finally { + if (Test-Path $TestDir) { Remove-Item $TestDir -Recurse -Force } + } + } + + It 'Handles mixed content correctly' { + $TestDir = Join-Path $TempDir 'encoding-test-mixed' + if (Test-Path $TestDir) { Remove-Item $TestDir -Recurse -Force } + New-Item -Path $TestDir -ItemType Directory | Out-Null + + try { + $File = Join-Path $TestDir 'unicode-test.ps1' + $text = 'Write-Host "Zażółć gęślą jaźń"' # Polish text with Unicode characters + [System.IO.File]::WriteAllText($File, $text, [System.Text.UTF8Encoding]::new($false)) + + # Import module with PassThru to access private functions for verification + $Module = Import-Module $ModuleToLoad -Force -PassThru + + # Should be detected as UTF8 due to Unicode characters + $encBefore = & $Module Get-FileEncoding -Path $File + $encBefore | Should -Be 'UTF8' + + # Convert to UTF8BOM + Convert-ProjectEncoding -Path $TestDir -ProjectType PowerShell -TargetEncoding UTF8BOM -Force -WhatIf:$false + + # Should now be UTF8BOM + $encAfter = & $Module Get-FileEncoding -Path $File + $encAfter | Should -Be 'UTF8BOM' + + # Content should remain unchanged + $content = Get-Content -LiteralPath $File -Raw -Encoding UTF8 + $content.Trim() | Should -Be $text + + } finally { + if (Test-Path $TestDir) { Remove-Item $TestDir -Recurse -Force } + } + } + + It 'Skips files when encoding does not match and Force is not used' { + $TestDir = Join-Path $TempDir 'encoding-test-skip' + if (Test-Path $TestDir) { Remove-Item $TestDir -Recurse -Force } + New-Item -Path $TestDir -ItemType Directory | Out-Null + + try { + $File = Join-Path $TestDir 'test.ps1' + [System.IO.File]::WriteAllText($File, 'Write-Host "Test"', [System.Text.UTF8Encoding]::new($false)) + $beforeBytes = [System.IO.File]::ReadAllBytes($File) + + # Try to convert without Force - should skip + Convert-ProjectEncoding -Path $TestDir -ProjectType PowerShell -SourceEncoding UTF8BOM -TargetEncoding UTF8 -WhatIf:$false + + $afterBytes = [System.IO.File]::ReadAllBytes($File) + $afterBytes | Should -Be $beforeBytes + + } finally { + if (Test-Path $TestDir) { Remove-Item $TestDir -Recurse -Force } + } + } +}