Use PowerShell to find and replace hex values in binary files [duplicate] - powershell

This question already has answers here:
delete some sequence of bytes in Powershell [duplicate]
(1 answer)
Methods to hex edit binary files via Powershell
(4 answers)
Closed 3 years ago.
UPDATE:
I got a working script to accomplish the task. I needed to batch process a bunch of files so it accepts a csv file formatted as FileName,OriginalHEX,CorrectedHEX. It's very slow even after limiting the search to the first 512 bytes. It could probably be written better and made faster. Thanks for the help.
UPDATE 2: Revised the search method to be faster but it's nowhere near as fast as a dedicated hex editor. Be aware that it's memory intensive. peaks around 32X the size of the file in RAM. 10MB=320MB of RAM. 100MB=3.2GB of RAM. I don't recommend for big files. It also saves to a new file instead of overwriting. Original file renamed as File.ext_original#date-time.
Import-CSV $PSScriptRoot\HEXCorrection.csv | ForEach-Object {
$File = $_.'FileName'
$Find = $_.'OriginalHEX'
$Replace = $_.'CorrectedHEX'
IF (([System.IO.File]::Exists("$PSScriptRoot\$File"))) {
$Target = (Get-ChildItem -Path $PSScriptRoot\$File)
} ELSE {
Write-Host $File "- File Not Found`n" -ForegroundColor 'Red'
RETURN
}
Write-Host "File: "$Target.Name`n"Find: "$Find`n"Replace: "$Replace
$TargetLWT = $Target.LastWriteTime
$TargetCT = $Target.CreationTime
IF ($Target.IsReadOnly) {
Write-Host $Target.Name "- Is Read-Only`n" -ForegroundColor 'Red'
RETURN
}
$FindLen = $Find.Length
$ReplaceLen = $Replace.Length
$TargetLen = (1..$Target.Length)
IF (!($FindLen %2 -eq 0) -OR !($ReplaceLen %2 -eq 0) -OR
[String]::IsNullOrEmpty($FindLen) -OR [String]::IsNullOrEmpty($ReplaceLen)) {
Write-Host "Input hex values are not even or empty" -ForegroundColor 'DarkRed'
RETURN
} ELSEIF (
$FindLen -ne $ReplaceLen) {
Write-Host "Input hex values are different lengths" -ForegroundColor 'DarkYellow'
RETURN
}
$FindAsBytes = New-Object System.Collections.ArrayList
$Find -split '(.{2})' | ? {$_} | % { $FindAsBytes += [Convert]::ToInt64($_,16) }
$ReplaceAsBytes = New-Object System.Collections.ArrayList
$Replace -split '(.{2})' | ? {$_} | % { $ReplaceAsBytes += [Convert]::ToInt64($_,16) }
# ^-- convert to base 10
Write-Host "Starting Search"
$FileBytes = [IO.File]::ReadAllBytes($Target)
FOREACH ($Byte in $FileBytes) {
$ByteCounter++
IF ($Byte -eq [INT64]$FindAsBytes[0]) { TRY {
(1..([INT64]$FindAsBytes.Count-1)) | % {
$Test = ($FileBytes[[INT64]$ByteCounter-1+[INT64]$_] -eq $FindAsBytes[$_])
IF ($Test -ne 'True') {
THROW
}
}
Write-Host "Found at Byte:" $ByteCounter -ForegroundColor 'Green'
(0..($ReplaceAsBytes.Count-1)) | % {
$FileBytes[[INT64]$ByteCounter+[INT64]$_-1] = $ReplaceAsBytes[$_]}
$Found = 'True'
$BytesReplaces = $BytesReplaces + [INT64]$ReplaceAsBytes.Count
}
CATCH {}
}
}
IF ($Found -eq 'True'){
[IO.File]::WriteAllBytes("$Target-temp", $FileBytes)
$OriginalName = $Target.Name+'_Original'+'#'+(Get-Date).ToString('yyMMdd-HHmmss')
Rename-Item -LiteralPath $Target.FullName -NewName $OriginalName
Rename-Item $Target"-temp" -NewName $Target.Name
#Preserve Last Modified Time
$Target.LastWriteTime = $TargetLWT
$Target.CreationTime = $TargetCT
Write-Host $BytesReplaces "Bytes Replaced" -ForegroundColor 'Green'
Write-Host "Original saved as:" $OriginalName
} ELSE {
Write-Host "No Matches" -ForegroundColor 'Red'}
Write-Host "Finished Search`n"
Remove-Variable -Name * -ErrorAction SilentlyContinue
} # end foreach from line 1
PAUSE
Original: This has been asked before but found no solutions to perform a simple and straight up find hex value and replace hex value on large files, 100MB+.
Even better would be any recommendations for a hex editor with command line support for this task.

Here's a first crack at it:
(get-content -encoding byte file) -replace '\b10\b',11 -as 'byte[]'
I was checking those other links, but the only answer that does search and replace has some bugs. I voted to reopen. The mklement0 one is close. None of them search and then print the position of the replacement.
Nevermind. Yours is faster and uses less memory.

Related

way to determine if file is PDF faster

Looking for some pointers / tips to increase the speed and/or efficacy of below. Would be open to other methods, but have only dabbled in powershell,cmd and python.
Also credit where credit is due: This is a hack-job on the following: https://stackoverflow.com/a/44183234/12834479
Rather than working local, I'm hitting a Network Share over VPN with abysmal connection speeds.
Roughly, it's working at 8 secs / PDF.
Issues I've tried to take care of, goal is to ensure each PDF is readable by Adobe. Images saved as PDF (but not pdfs) will open in some PDF software, but Adobe hates them. I have the method to convert, but my rate limiter is identifying them.
Adobe PDFs -start with %PDF
Some Bank PDFs - start with "blank space" then %PDF
3rd party software - Junk Headers, but %PDF is within document
$items = Get-ChildItem | Where-Object {$_.Extension -eq ".pdf"}
$arrary = #()
$logFile = "RESULTS_$(get-date -Format yyyymmdd).log"
$badCounter = 0
$goodCounter = 0
$msg = "`n`nProcessing " + $items.count + " files... "
Write-Host -nonewline -foregroundcolor Yellow $msg
foreach ($item in $items)
{
trap { Write-Output "Error trapped: $_"; continue; }
try {
$pdfText = Get-Content $item -raw
$ptr3 = '%PDF'
if ('%PDF' -ne $pdfText.SubString(([System.Math]::Max(0,$pdfText.IndexOf($ptr3))),4)) { $arrary+= "$item |-failed" >>$logfile;$badCounter += 1; $badCounter} else { $goodCounter += 1; $goodCounter}
continue;}
catch [System.Exception]{write-output "$item $_";}}
$totalCounter = $badCounter + $goodCounter
Write-Output $arrary >> $logFile
1..3 | %{ Write-Output "" >> $logFile }
Write-Output "Total: $totalCounter / BAD: $badCounter / GOOD: $goodCounter" >> $logFile
Write-Output "DONE!`n`n"
If any difference currently running in PS Version 7.1.3 / but also have 5.1.18 on local.
Actually, PDF files aren't plaintext files at all, but binary files, so you should not read them in as string.
What you are looking for is called a FourCC magic number in the file. This four-character code can be seen as Magic number to identify the file type.
For PDF files, these 4 bytes are 0x25, 0x50, 0x44, 0x46 ("%PDF") and the file should start with those bytes.
For those true PDF files, you could test with:
[byte[]]$fourCC = Get-Content -Encoding Byte -ReadCount 4 -TotalCount 4 -Path 'X:\TheFile.pdf'
if ([System.Text.Encoding]::ASCII.GetString($fourCC) -ceq '%PDF') {
Write-Host "This is a true PDF file"
}
However, as you say "Bank pdf's usually start with a blank space", to also consider those files "good", you can do:
[byte[]]$sixCC = Get-Content -Encoding Byte -ReadCount 6 -TotalCount 6 -Path 'X:\TheFile.pdf'
if ([System.Text.Encoding]::ASCII.GetString($sixCC) -cmatch '%PDF') {
Write-Host "This is a PDF file"
}
If you also want to treat files where "%PDF" is found anyhere in the file as "good", you will need to read the whole file as string, but with a one-to-one byte mapping of the bytes.
For that you can use below helper function:
function ConvertTo-BinaryString {
# converts the bytes of a file to a string that has a
# 1-to-1 mapping back to the file's original bytes.
# Useful for performing binary regular expressions.
Param (
[Parameter(Mandatory = $True, ValueFromPipeline = $True, Position = 0)]
[ValidateScript( { Test-Path $_ -PathType Leaf } )]
[String]$Path
)
# Note: Codepage 28591 returns a 1-to-1 char to byte mapping
$Encoding = [Text.Encoding]::GetEncoding(28591)
$Stream = [System.IO.FileStream]::new($Path, 'Open', 'Read')
$StreamReader = [System.IO.StreamReader]::new($Stream, $Encoding)
$BinaryText = $StreamReader.ReadToEnd()
$StreamReader.Close()
$Stream.Close()
return $BinaryText
}
Next, you can use that function as:
$binString = ConvertTo-BinaryString -Path 'X:\TheFile.pdf'
if ($binString.IndexOf("%PDF") -ge 0) {
Write-Host "This is a PDF file"
}
Putting it all together and assuming you want all files marked as .PDF files where the magic number '%PDF' (case-sensitive) can be found anywhere in the file:
function ConvertTo-BinaryString {
# converts the bytes of a file to a string that has a
# 1-to-1 mapping back to the file's original bytes.
# Useful for performing binary regular expressions.
Param (
[Parameter(Mandatory = $True, ValueFromPipeline = $True, Position = 0)]
[ValidateScript( { Test-Path $_ -PathType Leaf } )]
[String]$Path
)
# Note: Codepage 28591 returns a 1-to-1 char to byte mapping
$Encoding = [Text.Encoding]::GetEncoding(28591)
$Stream = [System.IO.FileStream]::new($Path, 'Open', 'Read')
$StreamReader = [System.IO.StreamReader]::new($Stream, $Encoding)
$BinaryText = $StreamReader.ReadToEnd()
$StreamReader.Close()
$Stream.Close()
return $BinaryText
}
$badCounter = 0
$goodCounter = 0
$logFile = "RESULTS_{0:yyyyMMdd}.log" -f (Get-Date)
# get an array of pdf file FullNames
$files = #(Get-ChildItem -File -Filter '*.pdf').FullName
Write-Host "Processing $($files.Count) files... " -ForegroundColor Yellow
# loop through the array, test if '%PDF' is found and output strings for the log file
$result = foreach ($item in $files) {
$pdfText = ConvertTo-BinaryString -Path $item
if ($pdfText.IndexOf("%PDF") -ge 0) {
$goodCounter++
"Success - $item"
}
else {
$badCounter++
"Fail - $item"
}
}
# write the output to the log file
$result | Set-Content -Path $logFile
"=" * 25 | Add-Content -Path $logFile
"BAD: $badCounter" | Add-Content -Path $logFile
"GOOD: $goodCounter" | Add-Content -Path $logFile
"Total: $($files.Count)" | Add-Content -Path $logFile
Write-Host "DONE!" -ForegroundColor Green

Powershell - Make a menu out of text file

In my adventure trying to learn Powershell, I am working on an extension on a script I have made. The idea is to make script there by adding ".iso" files into a folder. It will use that content in a menu so that I later can use it to select an iso file for a WM in Hyper-V
This is my version of how it will get the content in the first place
Get-ChildItem -Path C:\iso/*.iso -Name > C:\iso/nummer-temp.txt
Add-Content -Path C:\iso/nummer.txt ""
Get-Content -Path C:\iso/nummer-temp.txt | Add-Content -Path C:\iso/nummer.txt
When this code is run it will send an output like what i want. But my question is how do I use this output in a menu?
This is the best practice way to do so in powershell :
#lets say your .txt files gets this list after running get-content
$my_isos = $('win7.iso','win8.iso','win10.iso')
$user_choice = $my_isos | Out-GridView -Title 'Select the ISO File you want' -PassThru
#waiting till you choose the item you want from the grid view
Write-Host "$user_choice is going to be the VM"
I wouldn't try to make it with System.windows.forms utilities as i mentioned in my comment, unless you want to present the form more "good looking".
If you don't want to go for a graphical menu, but rather a console menu, you could use this function below:
function Show-Menu {
Param(
[Parameter(Position=0, Mandatory=$True)]
[string[]]$MenuItems,
[string] $Title
)
$header = $null
if (![string]::IsNullOrWhiteSpace($Title)) {
$len = [math]::Max(($MenuItems | Measure-Object -Maximum -Property Length).Maximum, $Title.Length)
$header = '{0}{1}{2}' -f $Title, [Environment]::NewLine, ('-' * $len)
}
# possible choices: digits 1 to 9, characters A to Z
$choices = (49..57) + (65..90) | ForEach-Object { [char]$_ }
$i = 0
$items = ($MenuItems | ForEach-Object { '{0} {1}' -f $choices[$i++], $_ }) -join [Environment]::NewLine
# display the menu and return the chosen option
while ($true) {
cls
if ($header) { Write-Host $header -ForegroundColor Yellow }
Write-Host $items
Write-Host
$answer = (Read-Host -Prompt 'Please make your choice').ToUpper()
$index = $choices.IndexOf($answer[0])
if ($index -ge 0 -and $index -lt $MenuItems.Count) {
return $MenuItems[$index]
}
else {
Write-Warning "Invalid choice.. Please try again."
Start-Sleep -Seconds 2
}
}
}
Having that in place, you call it like:
# get a list if iso files (file names for the menu and full path names for later handling)
$isoFiles = Get-ChildItem -Path 'D:\IsoFiles' -Filter '*.iso' -File | Select-Object Name, FullName
$selected = Show-Menu -MenuItems $isoFiles.Name -Title 'Please select the ISO file to use'
# get the full path name for the chosen file from the $isoFiles array
$isoToUse = ($isoFiles | Where-Object { $_.Name -eq $selected }).FullName
Write-Host "`r`nYou have selected file '$isoToUse'"
Example:
Please select the ISO file to use
---------------------------------
1 Win10.iso
2 Win7.iso
3 Win8.iso
Please make your choice: 3
You have selected file 'D:\IsoFiles\Win8.iso'

Powershell Host File edit

Guys i'm having some issues converting my Perl script to powershell, I need some help. In the host file of our machines, we have all of the URL's to our test environments blocked. In my PERL script, based on which environment is selected, it will comment out the line of the environment selected to allow access and block others so the testers can't mistakenly do things in the wrong environment.
I need help converting to powershell
Below is what I have in PERL:
sub editHosts {
print "Editing hosts file...\n";
my $file = 'C:\\Windows\\System32\\Drivers\\etc\\hosts';
my $data = readFile($file);
my #lines = split /\n/, $data;
my $row = '1';
open (FILE, ">$file") or die "Cannot open $file\n";
foreach my $line (#lines) {
if ($line =~ m/$web/) {
print FILE '#'."$line\n"; }
else {
if ($row > '21') {
$line =~ s/^\#*127\.0\.0\.1/127\.0\.0\.1/;
$line =~ s/[#;].*$//s; }
print FILE "$line\n"; }
$row++;
}
close(FILE);
}
Here is what i've tried in Powershell:
foreach ($line in get-content "C:\windows\system32\drivers\etc\hosts") {
if ($line -contains $web) {
$line + "#"
}
I've tried variation including set-content with what used to be in the host file, etc.
Any help would be appreciated!
Thanks,
Grant
-contains is a "set" operator, not a substring operator. Try .Contains() or -like.
This will comment out lines matching the variable $word, while removing # from non-matches (except the header):
function Edit-Hosts ([string]$Web, $File = "C:\windows\system32\drivers\etc\hosts") {
#If file exists and $web is not empty/whitespace
if((Test-Path -Path $file -PathType Leaf) -and $web.Trim()) {
$row = 1
(Get-Content -Path $file) | ForEach-Object {
if($_ -like "*$web*") {
#Matched PROD, comment out line
"#$($_)"
} else {
#No match. If past header = remove comment
if($row -gt 21) { $_ -replace '^#' } else { $_ }
}
$row++
} | Set-Content -Path $file
} else {
Write-Error -Category InvalidArgument -Message "'$file' doesn't exist or Web-parameter is empty"
}
}
Usage:
Edit-Hosts -Web "PROD"
This is a similar answer to Frode F.'s answer, but I'm not yet able to comment to add my 2c worth, so have to provide an alternative answer instead.
It looks like one of the gotchas moving from perl to PowerShell, in this example, is that when we get the content of the file using Get-Content it is an "offline" copy, i.e. any edits are not made directly to the file itself. One approach is to compile the new content to the whole file and then write that back to disk.
I suppose that the print FILE "some text\n"; construct in perl might be similar to "some text" | Out-File $filename -Encoding ascii -Append in PowerShell, albeit you would use the latter either (1) to write line-by-line to a new/empty file or (2) accept that you are appending to existing content.
Two other things about editing the hosts file:
Be sure to make sure that your hosts file is ASCII encoded; I have caused a major outage for a key enterprise application (50k+ users) in learning that...
You may need to remember to run your PowerShell / PowerShell ISE by right-clicking and choosing Run as Administrator else you might not be able to modify the file.
Anyway, here's a version of the previous answer using Out-File:
$FileName = "C:\windows\system32\drivers\etc\hosts"
$web = "PROD"
# Get "offline" copy of file contents
$FileContent = Get-Content $FileName
# The following creates an empty file and returns a file
# object (type [System.IO.FileInfo])
$EmptyFile = New-Item -Path $FileName -ItemType File -Force
foreach($Line in $FileContent) {
if($Line -match "$web") {
"# $Line" | Out-File $EmptyFile -Append -Encoding ascii
} else {
"$Line" | Out-File $EmptyFile -Append -Encoding ascii
}
}
Edit
The ($Line -match "$web") takes whatever is in the $web variable and treats it as a regular expression. In my example I was assuming that you were just wanting to match a simple text string, but you might well be trying to match an IP address, etc. You have a couple of options:
Use ($Line -like "*$web*") instead.
Convert what is in $web to be an escaped regex, i.e. one that will match literally. Do this with ($Line -match [Regex]::Escape($web)).
You also wanted to strip off comments from any line past row 21 of the hosts file, should that line not match $web. In perl you have used the s substitution operator; the PowerShell equivalent is -replace.
So... here is an updated version of that foreach loop:
$LineCount = 1
foreach($Line in $FileContent) {
if($Line -match [Regex]::Escape($web) {
# ADD comment to any matched line
$Line = "#" + $Line
} elseif($LineCount -gt 21) {
# Uncomment the other lines
$Line = $Line -replace '^[# ]+',''
}
# Remove 'stacked up' comment characters, if any
$Line = $Line -replace '[#]+','#'
$Line | Out-File $EmptyFile -Append -Encoding ascii
$LineCount++
}
More Information
Are there good references for moving from Perl to Powershell?
How to use operator '-replace' in PowerShell to replace strings of texts with special characters and replace successfully
about_Comparison_Operators
http://www.comp.leeds.ac.uk/Perl/sandtr.html
If you wanted to verify what was in there and then add entries, you could use the below which is designed to be ran interactively and returns any existing entries you specify in the varibles:
Note: the `t is powershell's in script method for 'Tab' command.
$hostscontent
# Script to Verify and Add Host File Entries
$hostfile = gc 'C:\Windows\System32\drivers\etc\hosts'
$hostscontent1 = $hostfile | select-string "autodiscover.XXX.co.uk"
$hostscontent2 = $hostfile | select-string "webmail.XXX.co.uk"
$1 = "XX.XX.XXX.XX`tautodiscover.XXX.co.uk"
$2 = "webmail.XXX.co.uk"
# Replace this machines path with a path to your list of machines e.g. $machines = gc \\machine\machines.txt
$machines = gc 'c:\mytestmachine.txt'
ForEach ($machine in $machines) {
If ($hostscontent1 -ne $null) {
Start-Sleep -Seconds 1
Write-Host "$machine Already has Entry $1" -ForegroundColor Green
} Else {
Write-Host "Adding Entry $1 for $machine" -ForegroundColor Green
Start-Sleep -Seconds 1
Add-Content -Path C:\Windows\System32\drivers\etc\hosts -Value "XX.XX.XXX.XX`tautodiscover.XXX.co.uk" -Force
}
If ($hostscontent2 -ne $null) {
Start-Sleep -Seconds 1
Write-Host "$machine Already has Entry $2" -ForegroundColor Green
} Else {
Write-Host "Adding Entry $2 for $machine" -ForegroundColor Green
Start-Sleep -Seconds 1
Add-Content -Path C:\Windows\System32\drivers\etc\hosts -Value "XX.XX.XXX.XX`twebmail.XXX.co.uk" -Force
}
}

How expressive can I get with a PowerShell Expression in Format-Table?

I have the following script that outputs a color coded folder hierarchy of a user's Exchange mailbox. It output the line in red if it's over a certain threshold (20 MB in this case) and gray if not.
#Get Folder Size Breakdown to Table with Color Coding
get-mailbox $username |
Get-MailboxFolderStatistics |
ft #{
Name="Name"
Expression=
{
$prefix=""
foreach($c in $_.FolderPath.ToCharArray())
{
if($c -eq '/'){$prefix+='-'}
}
if($_.FolderSize -gt 20MB)
{
$Host.UI.RawUI.ForegroundColor = "Red"
} else
{
$Host.UI.RawUI.ForegroundColor = "Gray"
}
$prefix + $_.Name
}
},
FolderSize,
FolderandSubfolderSize
There are a few problems with this script.
If the last folder processed is larger than 20 MB, my console text remains Red after it runs.
This script assumes that the original console text was Gray. If it's not Gray, then I've changed the user's console text.
Both of these are very easy to resolve if you're not in the context of a format-table expression, but I can't for the life of me figure out if it's possible to resolve these issues in this particular case. Here's the gist of what I've tried but it doesn't work. (In reality I've tried about 20 different variations).
get-mailbox $username |
Get-MailboxFolderStatistics |
ft #{
Name="Name"
Expression=
{
$prefix=""
$originalColor = $Host.UI.RawUI.ForegroundColor
foreach($c in $_.FolderPath.ToCharArray())
{
if($c -eq '/'){$prefix+='-'}
}
if($_.FolderSize -gt 20MB)
{
$Host.UI.RawUI.ForegroundColor = "Red"
}
$prefix + $_.Name
$Host.UI.RawUI.ForegroundColor = $originalColor
}
},
FolderSize,
FolderandSubfolderSize
Note: The purpose of this is to eventually compress this down to a one-liner. I know that I can store the variable before I start the pipeline and the restore the color after the pipeline is finished, but that takes the fun/aggravation out of it. I'm more curious as to whether or not I can accomplish this without altering the basic structure of this pipeline.
I don't think this is possible. Essentially, every time Format-Table reads the Expression for Name the foreground color will change. But Format-Table probably doesn't write out the value from that expression immediately, so you can't reset the color in the expression.
I think you're going to have to wrap your pipeline:
$originalColor = $Host.UI.RawUI.ForegroundColor
get-mailbox $username |
Get-MailboxFolderStatistics |
ft #{
Name="Name"
Expression=
{
$prefix = " " * (($_.FolderPath -split '/').Length)
$Host.UI.RawUI.ForegroundColor = if($_.FolderSize -gt 20MB) { "Red" } else { $originalColor }
$prefix + $_.Name
}
},
FolderSize,
FolderandSubfolderSize
$Host.UI.RawUI.ForegroundColor = $originalColor
Another option would be to write your own formatting code that finds the maximum size of each column then uses Write-Host to write things out:
$stats = get-mailbox $username |
Get-MailboxFolderStatistics |
$nameMaxWidth = 0
$sizeMaxWidth = 0
$subFolderSizeMaxWidth = 0
$stats | ForEach-Object {
if( $_.Name.Length -gt $nameMaxWidth )
{
$nameMaxWidth = $_.Name.Length + (($_.FolderPath -split '/').Length - 1)
}
$sizeWidth = $_.FolderSize.ToString().Length
if( $sizeWidth -gt $sizeMaxWidth )
{
$sizeMaxWidth = $sizeWidth
}
$subSizeWidth = $_.FolderAndSubFolderSize.ToString().Length
if( $subSizeWidth -gt $subFolderSizeMaxWidth )
{
$subFolderSizeMaxWidth = $subSizeWidth
}
}
$stats | ForEach-Object {
$colorParam = #{ }
if( $_.FolderSize -gt 20MB )
{
$colorParam.ForegroundColor = 'Red'
}
$prefix = ' ' * (($_.FolderPath -split '/').Length - 1)
Write-Host ("{0}{1,$nameMaxWidth}" -f $prefix,$_.Name) -NoNewLine #colorParam
Write-Host " " -NoNewline
Write-Host ("{0,-$sizeMaxWidth}" -f $_.FolderSize) -NoNewLine
Write-Host " " -NoNewLine
Write-Host ("{0,-$subFolderSizeMaxWidth}" -f $_.FolderAndSubFolderSize)
}

Powershell colored directory listing is incorrect with format-wide

I got this colored dir script from http://tasteofpowershell.blogspot.com/2009/02/get-childitem-dir-results-color-coded.html:
function ls {
$regex_opts = ([System.Text.RegularExpressions.RegexOptions]::IgnoreCase -bor [System.Text.RegularExpressions.RegexOptions]::Compiled)
$fore = $Host.UI.RawUI.ForegroundColor
$compressed = New-Object System.Text.RegularExpressions.Regex('\.(zip|tar|gz|rar)$', $regex_opts)
$executable = New-Object System.Text.RegularExpressions.Regex('\.(exe|bat|cmd|ps1|psm1|vbs|rb|reg|dll|o|lib)$', $regex_opts)
$executable = New-Object System.Text.RegularExpressions.Regex('\.(exe|bat|cmd|ps1|psm1|vbs|rb|reg|dll|o|lib)$', $regex_opts)
$source = New-Object System.Text.RegularExpressions.Regex('\.(py|pl|cs|rb|h|cpp)$', $regex_opts)
$text = New-Object System.Text.RegularExpressions.Regex('\.(txt|cfg|conf|ini|csv|log|xml)$', $regex_opts)
Invoke-Expression ("Get-ChildItem $args") |
%{
if ($_.GetType().Name -eq 'DirectoryInfo') {
$Host.UI.RawUI.ForegroundColor = 'DarkCyan'
$_
$Host.UI.RawUI.ForegroundColor = $fore
} elseif ($compressed.IsMatch($_.Name)) {
$Host.UI.RawUI.ForegroundColor = 'Yellow'
$_
$Host.UI.RawUI.ForegroundColor = $fore
} elseif ($executable.IsMatch($_.Name)) {
$Host.UI.RawUI.ForegroundColor = 'Red'
$_
$Host.UI.RawUI.ForegroundColor = $fore
} elseif ($text.IsMatch($_.Name)) {
$Host.UI.RawUI.ForegroundColor = 'Green'
$_
$Host.UI.RawUI.ForegroundColor = $fore
} elseif ($source.IsMatch($_.Name)) {
$Host.UI.RawUI.ForegroundColor = 'Cyan'
$_
$Host.UI.RawUI.ForegroundColor = $fore
} else {
$_
}
}
}
It works great, but I most of the time I want only the file names, in wide format. So after the invoke-expression call, I added
Invoke-Expression ("Get-ChildItem $args") |
%{
if ($_.GetType().Name -eq 'DirectoryInfo') {
:
:
:
$_
}
} | format-wide -property Name
}
Now I have a bug. Only the colour of the second column is correct; the first item in each column takes the colour of the item in the second column. For example, if I have
> ls
Directory Program.exe
Then both Directory and Program.exe will be red, even though Directory is supposed to be DarkCyan. How can I correct this?
Rather than twiddling the foreground/background colors of the host in between displaying text to the screen, why don't you use Write-Host which gives you a bit more control over the displayed text (you can control when newlines are output) e.g.:
$_ | Out-String -stream | Write-Host -Fore Red
And for the wide listing use, you will need to handle the column formatting yourself unless you want to update the format data XML for the DirectoryInfo/FileInfo types. If you don't want to do that, then you can write out each name - padded out appropriately - with the desired color. On the last column, set the -NoNewLine param to $false:
$width = $host.UI.RawUI.WindowSize.Width
$cols = 3
ls | % {$i=0; $pad = [int]($width/$cols) - 1} `
{$nnl = ++$i % $cols -ne 0; `
Write-Host ("{0,-$pad}" -f $_) -Fore Green -NoNewLine:$nnl}
Just thought I would point you to this question I posted which outputs linux style colored output and format in columns correctly. How to write a list sorted lexicographically in a grid listed by column?