Utils/FileTools.ps1

# =============================================================================
# Created On: 2018/06/20 @ 12:52
# Created By: Alcha
# Organization: HassleFree Solutions, LLC
# Filename: FileTools.ps1
# Description: Contains various functions for modifying/working with files.
# =============================================================================

<#
.SYNOPSIS
  Updates/replaces the given character(s) in all the filenames in the given path.

.DESCRIPTION
  Updates/replaces the given character(s) for the new given character in all the
filenames present in the given path. If the Recurse switch parameter is
provided, then every file in every subdirectory is updated as well.

.PARAMETER Path
  The path to where the files you wish to udpate are.

.PARAMETER OldValues
  The character(s) in the filenames that you wish to replace.

.PARAMETER NewValue
  The character(s) in the filenames that you wish to be used as the new filename
value.

.PARAMETER Recurse
  Determines if you wish to update all files in subdirectories as well as the
provided one.

.EXAMPLE
  PS C:\> Update-FilenameCharacter -Path C:\Development -OldValue " " -New "_"
Updates all the files in the C:\Development directory (none of the
subdirectories) so that any filenames with a space, has the space replaced with
an underscore.

.EXAMPLE
  PS C:\> Update-Filename -Path C:\Development -Old " " -New "." -Recurse
Updates all files in the C:\Development directory, and every subdirectory within
it, so that any filenames with a space present has the space replaced with a
period.

.EXAMPLE
  PS C:\> Update-Filename -Path C:\Development -Old " ", "_" -New "-" -Recurse
Updates all files in the C:\Development directory, and every subdirectory within
it, so that any filenames with a space or underscore present has that character
replaced with a hyphen.
#>

function Update-FilenameCharacter {
  [CmdletBinding()]
  [Alias('Update-Filename', 'Replace-FilenameCharacter')]
  param (
    [Parameter(Position = 0)]
    [Alias('Dir', 'Directory')]
    [String]
    $Path = ".",

    [Parameter(Position = 1)]
    [Alias('Old')]
    [String]
    $OldValue = " ",

    [Parameter(Position = 2)]
    [Alias('New')]
    [String]
    $NewValue = "_",

    [Parameter(Position = 3)]
    [Switch]
    $Recurse
  )

  if ($Recurse.IsPresent) {
    $Files = Get-ChildItem -Path $Path -Recurse
  } else {
    $Files = Get-ChildItem -Path $Path
  }

  foreach ($File in $Files) {
    $OldName = $File.FullName
    foreach ($OldValue in $OldValues) {
      $NewName = $OldName
      if ($OldName -like "*$OldValue*") {
        $NewName = $OldName -replace $OldValue, $NewValue
      }

      if ($NewName -ne $OldName) {
        Move-Item $File.FullName $NewName

        Write-Output "OldName = $OldName"
        Write-Output "NewName = $NewName`n"
      }
    }
  }
}

<#
.SYNOPSIS
  Trims every line of the files in the given directory.

.DESCRIPTION
  Iterates through every file with a .ps1, .psd1, .psm1, .txt, or .js extension
in the given directory and then reads it line by line. Each line then has
the Trim() function called on it, in order to remove any excess whitespace.

.PARAMETER Path
  The directory containing the files you wish to cleanup.

.PARAMETER FileTypes
  An array of strings containing the file types to filter for before cleaning.
If this parameter is left empty, the following defaults are used: '*.ps1',
'*.psd1', '*.psm1', '*.txt', '*.js', '*.json'

.PARAMETER Recurse
  Determines if you wish to clean all files in subdirectories as well as the
provided one.

.EXAMPLE
  PS C:\> Remove-ExcessWhitespace -Path C:\Development\Projects\Gamgee

.EXAMPLE
  PS C:\Development\Projects\> rew .\Gamgee -Recurse
Removes all the excess whitespace from each file within the
C:\Development\Projects\Gamgee directory and every subdirectory within it.

.EXAMPLE
  PS C:\> rew C:\Development\Projects\Gamgee -FileTypes '*.txt' -Recurse
Removes all the excess whitespace from each .txt file within the given directory
and every subdirectory within it.
#>

function Remove-ExcessWhitespace {
  [CmdletBinding()]
  [Alias('Clean-Whitespace', 'rew', 'Clean-Files')]
  param (
    [Parameter(Mandatory = $true,
      ValueFromPipeline = $true,
      ValueFromPipelineByPropertyName = $true,
      ValueFromRemainingArguments = $true,
      Position = 0,
      HelpMessage = 'Where are the files you wish to clean up?')]
    [ValidateScript( { return Test-Path $_ })]
    [Alias('FilePath', 'FileDir', 'Dir')]
    [System.String]
    $Path,

    [Parameter(Mandatory = $false, Position = 1,
      HelpMessage = 'What file types do you wish to clean?')]
    [System.String[]]
    $FileTypes,

    [Parameter(Mandatory = $false, Position = 2)]
    [Switch]
    $Recurse
  )

  begin {
    if ($FileTypes.Length -eq 0) {
      $FileTypes = @('*.ps1', '*.psd1', '*.psm1', '*.txt', '*.js', '*.json')
    }

    if ($Recurse.IsPresent) {
      $ScriptFiles = Get-ChildItem -Path $Path -Include $FileTypes -Recurse
    } else {
      $ScriptFiles = Get-ChildItem -Path $Path -Include $FileTypes
    }
  }

  process {
    foreach ($Script in $ScriptFiles) {
      $Content = Get-Content $Script.FullName
      Write-Verbose "Processing $Script..."

      for ($x = 0; $x -lt $Content.Length; $x++) {
        $Line = $Content[$x].ToString()
        $Content[$x] = $Line.TrimEnd() + "`n"
      }

      Out-File -FilePath $Script.FullName -InputObject $Content -Encoding 'utf8' -NoNewline
    }
  }

  end {
    Write-Verbose "Completed processing $($ScriptFiles.Length) files..."
  }
}

<#
.SYNOPSIS
  Reads the BOM of the given file(s) and tries to determine the file encoding.

.DESCRIPTION
  Reads the BOM of the given file(s) and tries to determine the file encoding.
If the encoding cannot be determined, it is set as "unknown". For every path
provided, a PSCustomObject is returned containing two values, the full FilePath,
and the encoding. (e.g. @{"File" = C:\Temp\Test.txt; "Encoding" = 'UTF8'})

.PARAMETER Path
  The path(s) to the file you wish to determine the encoding of as a String
object.

.PARAMETER FileInfo
  The path(s) to the file you wish to determine the encoding of as a FileInfo
object.

.EXAMPLE
  PS C:\> Get-FileEncoding -Path 'C:\Temp\Test.txt'
Gets the encoding of the Test.txt file and returns the encoding like so:
File Encoding
---- --------
C:\Temp\Test.txt UTF8
#>

function Get-FileEncoding {
  [CmdletBinding(DefaultParameterSetName = 'PathSet')]
  [OutputType([PSCustomObject])]
  param (
    [Parameter(ParameterSetName = 'PathSet',
      Mandatory = $true,
      ValueFromPipeline = $true,
      Position = 0)]
    [System.String[]]
    $Path,

    [Parameter(ParameterSetName = 'FileInfoSet',
      Mandatory = $true,
      ValueFromPipeline = $true,
      Position = 0)]
    [System.IO.FileInfo[]]
    $FileInfo
  )

  begin {
    if ($Path) {
      $FilePaths = Get-ChildItem $Path | ForEach-Object FullName
    } else {
      #$FileInfo
      $FilePaths = $FileInfo.FullName
    }

    if (!($FilePaths)) {
      throw "No filepaths found."
    }
  }

  process {
    foreach ($FilePath in $FilePaths) {
      [System.Byte[]]$Byte = Get-Content -Encoding byte -ReadCount 4 -TotalCount 4 -Path $FilePath

      if ($Byte[0] -eq 0xef -and $Byte[1] -eq 0xbb -and $Byte[2] -eq 0xbf) {
        # EF BB BF (UTF8)
        $Encoding = 'UTF8'
      } elseif ($Byte[0] -eq 0xfe -and $Byte[1] -eq 0xff) {
        # FE FF (UTF-16 Big-Endian)
        $Encoding = 'Unicode UTF-16 Big-Endian'
      } elseif ($Byte[0] -eq 0xff -and $Byte[1] -eq 0xfe) {
        # FF FE (UTF-16 Little-Endian)
        $Encoding = 'Unicode UTF-16 Little-Endian'
      } elseif ($Byte[0] -eq 0 -and $Byte[1] -eq 0 -and $Byte[2] -eq 0xfe -and $Byte[3] -eq 0xff) {
        # 00 00 FE FF (UTF32 Big-Endian)
        $Encoding = 'UTF32 Big-Endian'
      } elseif ($Byte[0] -eq 0xfe -and $Byte[1] -eq 0xff -and $Byte[2] -eq 0 -and $Byte[3] -eq 0) {
        # FE FF 00 00 (UTF32 Little-Endian)
        $Encoding = 'UTF32 Little-Endian'
      } elseif ($Byte[0] -eq 0x2b -and $Byte[1] -eq 0x2f -and $Byte[2] -eq 0x76 -and ($Byte[3] -eq 0x38 -or $Byte[3] -eq 0x39 -or $Byte[3] -eq 0x2b -or $Byte[3] -eq 0x2f)) {
        # 2B 2F 76 (38 | 38 | 2B | 2F)
        $Encoding = 'UTF7'
      } elseif ($Byte[0] -eq 0xf7 -and $Byte[1] -eq 0x64 -and $Byte[2] -eq 0x4c) {
        # F7 64 4C (UTF-1)
        $Encoding = 'UTF-1'
      } elseif ($Byte[0] -eq 0xdd -and $Byte[1] -eq 0x73 -and $Byte[2] -eq 0x66 -and $Byte[3] -eq 0x73) {
        # DD 73 66 73 (UTF-EBCDIC)
        $Encoding = 'UTF-EBCDIC'
      } elseif ($Byte[0] -eq 0x0e -and $Byte[1] -eq 0xfe -and $Byte[2] -eq 0xff) {
        # 0E FE FF (SCSU)
        $Encoding = 'SCSU'
      } elseif ($Byte[0] -eq 0xfb -and $Byte[1] -eq 0xee -and $Byte[2] -eq 0x28) {
        # FB EE 28 (BOCU-1)
        $Encoding = 'BOCU-1'
      } elseif ($Byte[0] -eq 0x84 -and $Byte[1] -eq 0x31 -and $Byte[2] -eq 0x95 -and $Byte[3] -eq 0x33) {
        # 84 31 95 33 (GB-18030)
        $Encoding = 'GB-18030'
      } else {
        $Encoding = 'Unknown'
      }

      return [PSCustomObject]@{
        "File" = $FilePath; "Encoding" = $Encoding
      }
    }
  }
}