Thursday, November 8, 2012

HTML5 Canvas

Well, it looks like I need to revisit my syntax color copy for PS3.  I'll put it on the list.

But for now, Eric  (http://ericsowell.com/) did a presentation at NorthDallas.NET last night about the Canvas via Java.  Not .NET really, but I didn't even think to harass him about it.  So while he was talking I was trying to get it accessible from powershell.  I had the wrong version of IE accessible, but this morning in IE10.  I got it working.. Nothing rocket science, but I did manage to make an example as nothing showed up when I searched google for "HTML5 Canvas PowerShell"  This really is a bit of an edge case, I'll buy that.



$oie = new-object  -com internetexplorer.application
$oIE.navigate2("About:blank")
while ($oIE.busy) {
    sleep -milliseconds 50
}
$oIE.visible=$true

$doc = $oie.Document
$oIE.document.IHTMLDOcument2_write([string]'<body><canvas id="mycanvas" width="350" height="350">Test</canvas></body>')
while ($oIE.busy) {
    sleep -milliseconds 50
}
$canvas = $doc.getElementsByTagName("canvas")
$canvas = $doc.getElementByID("mycanvas")
$context  = $canvas.getContext("2d")
$context.clearRect(0, 0, $canvas.width, $canvas.height);
for ($i = 0; $i -lt 3; $i++)  {
            $context.fillStyle = "#95B524";
            $context.beginPath();
            $context.strokeStyle = "#fff";
            $context.lineWidth = 3;
            $context.arc(100,100,100,0,[math]::PI*2,$true);
            $context.closePath();
            $context.stroke();
            $context.fill();
        }

Thursday, May 31, 2012

Alternate to Jobs via a Batch and a Wait-Job to manage them.


We have a series of retrieve process that run each morning.  Most of these can run Async, but I have had two problems with using Jobs to accomplish this.

  1. If we pass simple variables to the job we have to make a DB call to get the full record and Dot Includes to the functions we have don't seem to want to work. 
  2. If we gather the details and populate the class to pass along we get serialization issues.
We should be able to resolve both items in the long run. In the short term however we have a batch file that runs multiple powershell.exe using "Start" to let them run async.  This is running on a server and the the some of the PowerShell scripts need to launch and interact with GUI applications.  So we have a scheduled job that calls Sysinternals psExec.exe to launch and RDC session off the console on the server.  Now I realize this sounds horrible to someone that doesn't work with me.  But this process creates a clean environment for the retrieves to work in.

Once the process is over the batch needs to log-out to release the session.  Simply waiting for the process to close doesn't seem to always cut it for us on 2003 Server.  But if Start has each one running on its own, we need to know when those are done.  So before we can call the ShutDown.exe logout command, we need to test that every powerShell session is closed.  And we need to make sure that only the sessions in this RDC are being checked.  It is helpful to mention that the server has 5 or 6 interactive users RDC in at times.


So:

001
002
003
004
005
006
007
008
009
010
#http://blogs.msdn.com/b/powershell/archive/2009/02/12/stopping-every-instance-of-powershell-exe-except-the-one-i-m-in.aspx
#$SessionID = Get-Process | Where-Object { $_.ID -eq $pid } | Select -expandproperty SessionID # 24 MilliSeconds
$SessionID = [System.Diagnostics.Process]::GetCurrentProcess() |
            Select -expandproperty SessionID# 15 MilliSeconds
Do
{
    Sleep -seconds 30
    $ActivePSEXE = Get-process PowerShell |
        Where {$_.SessionID -eq $SessionID -and $_.ID -ne $pid}
While ($ActivePSEXE)


------
Will asked why not use a threading approach, but these scripts load some generated code that has overlapping namespaces and they would fail to load if called from the same appdomain. (9:17a CT)



Monday, May 7, 2012

Table of an array

I had a a PSObject with to fields [bool]"Done" and [string]"Name".  To signify the status of some production items.
I need these results to be human readable and not print pages of data.
I don't propose that this is the best solution, but this is what I came up with as a quick solution.

This forces each name to a 20 character column and puts for columns on the page at once.


$data2 | Group {$_.Done} | %{
            $_.Name
            $ColumnCount = 4
            $Count = $_.Group.Count
            For  ($index = 0$index -le $Count;$index+=$ColumnCount)
            {            
              For ($i=0;$i -lt $columnCount;$i++)
              {
                if ($index+$i -lt $Count) {
                    Write-host $_.Group[$index+$i].Name.Padleft(20-NoNewline              
                }
              }
              Write-host            
            }
        }

Try number 2 breaks out the old modulus function.  I asked Will if he had an answer and did a quick "Is this what you mean?" using a 1..11 array.  Seeing the array as numbers modulus was the clear grouping method.


$data2 | Group {$_.Done} | %{
            Write-host $_.Name
            $ColumnCount = 6           
            For  ($index = 0$index -lt $_.Group.Count;$index++)
            {
              Write-host $_.Group[$index].Name.Padleft(20-NoNewline               
              if ($index $ColumnCount -eq 0) { Write-host }
            }
            Write-host
        }


I also considered, and I may yet revise my solution, creating a PSObject with 4 members and assign them in a similar fashion.  This would allow me to ConvertTo-CSV, and use the normal Format-Table.


Friday, April 20, 2012

Accessing old Assemblies

By "Accessing old Assemblies" I don't just mean that I am looking at a .NET 1.1 assembly.  I mean, this is the project that I wrote when we bought a VS2003 license and tried to leave VB6 behind us "OLD".

Of specific issue, I have some assemblies that are built from source by our process and loaded in VB.NET as a script.  They really do what I use PowerShell for now for other areas as they define specific logic and call into a central assembly to do anything complicated.  An instance is created and the "script" is passed a class of configuration properties and told to run.  The parent application then waits for completion and relays results back to an MSMQ where a VB6 application relays the results to a user.

This is an old and patchwork approach.   So I want to be able to remove the VB6 app and the MSMQ and call these compiled "scripts" from Powershell.  The problem is that I can't just New-Object without a namespace....  and I really have no idea what the namespace is.

Fortunately Add-Type has a PassThru argument.  What comes out of this is an array of RuntimeType.  Unfortunately it is an array and not a hash table.  Fortunately PowerShell is built for this.


001
002
003
$ScriptAssembly = add-type -Path "D:\source\MyScript.dll" -pass
$MyScript = new-object $($ScriptAssembly | Where {$_.Name -eq "MyScriptBaseClass"})
$MyScript.Run()


Now I don't have to find the namespace, just the name that you see when you write-output.



Write-output $ScriptAssembly



Sunday, April 1, 2012

Excel Part 2 - Import-Excel

Continuing from http://import-powershell.blogspot.com/2012/03/excel-part-1.html

Import-Excel - I have this broken into 3 basic parts.  Open/Close Excel, Populate Headers/ Member Names, and saving row data into the collection.  My original implementation pulled out an array of hash tables, but to be more effective I have started using a more true "Import" convention and output an array of PSObjects.  To maintain compatibility an argument will toggle between the two.

I am adding in another few helper functions.
Get-ExcelWorkSheet  helps directly retrieve the Sheet Object:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
function Get-ExcelWorkSheet {
Param([parameter(Mandatory=$true,ValueFromPipeline=$true)] $inputObject
  ,$SheetName
  ,[switch] $Visible
  ,[switch] $readonly)
  if ($inputObject -is [Microsoft.Office.Interop.Excel.Workbook]) {
    $WorkBook = $inputObject
  } else {
    $WorkBook = Get-ExcelWorkBook $inputObject -Visible:$Visible -readonly:$readonly
  }
  if (($SheetName -eq $null-or $SheetName -eq 0) {
    $WorkBook.ActiveSheet
  } else {
    $WorkBook.WorkSheets.item($SheetName)
  }
}


Import-Row will read a Row and output a hash table.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
Function Import-Row {
   Param($Row,[hashtable] $Headers =@{},$ColumnStart = 1,$ColumnCount = $Row.Value2.Count)
   $output = @{}
   for ($index=$ColumnStart;$index -le $ColumnCount;$index ++)
   {
        If ($Headers.Count -eq 0)
        {
            $Key = $Index
        } Else {
            $Key = $Headers[$index]
        }
        $output.Add($Key,$row.Cells.Item(1,$index).Text)
   }
   return $output
}

 I have a sample XLS like the following:
ANIMAL
ARMS
LEGS
TAIL
Dog
0
4
TRUE
Human
2
2
FALSE
Snake
0
0
TRUE

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
Function Import-Excel {
Param(
  [parameter(Mandatory=$true,ValueFromPipeline=$true)] $inputObject
,[Object] $SheetName
,[switch] $Visible
,[switch] $readonly
,[int] $startOnLineNumber =1
,[switch] $closeExcel
,[switch] $asHashTable
,[hashtable] $FieldNames =@{})
#Check what the input is.
if ($inputObject -is [Microsoft.Office.Interop.Excel.range]) {
$range = $inputObject
elseif ($inputObject -isnot [Microsoft.Office.Interop.Excel.Worksheet]) {
$WorkSheet = Get-ExcelWorkSheet $inputObject -SheetName $SheetName -Visible:$Visible -readonly:$readonly 
$range = $WorkSheet.UsedRange
else {
$WorkSheet = $inputObject
$range = $WorkSheet.UsedRange
}
# populate the Header
if ($FieldNames.Count -eq 0) {
       $FieldNames = Import-Row $range.Rows.Item($startOnLineNumber++)              
}

    for ($RowIndex=$startOnLineNumber;$RowIndex -le $range.Rows.Count;$RowIndex++) {
       $output = Import-Row $range.Rows.Item($RowIndex-Headers $FieldNames
    if ($asHashtAble) {
       Write-Output $output
    } else {
       New-Object PSObject -property $output
    }

# If we opened Excel, we should close Excel.
if ($closeExcel) {  
  $WorkSheet.Activate() | Out-Null
  Close-ExcelApplication $WorkSheet
}
}
Import-Excel "$PWD\Animal.xlsx" -Visible -closeExcel

Yields:

TAIL                                    ARMS                                  LEGS                                  ANIMAL                                  
----                                       ----                                       ----                                      ------                                  
TRUE                                       0                                          4                                         Dog                                    
FALSE                                     2                                          2                                         Human                                  
TRUE                                       0                                          0                                         Snake                                  

Saturday, March 31, 2012

Excel Part 1

The core MS Office apps have their application and inner objects exposed via COM.  These COM interfaces have distributable .NET Interop assemblies available to download.

There are two basic ways to interact with Excel via the COM objects and via the interop assembly.  Functionally I think the COM will allow you to accomplish the same tasks, but it will not be as easy.  To load the interop you will need:

001
002
003
004
005
006
007
#Load the Excel Assembly, Locally or from GAC
try {
    Add-Type -ASSEMBLY "Microsoft.Office.Interop.Excel"  | out-null
}catch {
#If the assembly can't be found this will load the most recent version in the GAC
    [Reflection.Assembly]::LoadWithPartialname("Microsoft.Office.Interop.Excel"| out-null
}

I tend to distribute the inerop DLL in my network share so I don't have to make sure that all servers and workstations have it installed.  The above should take care of either loading a local assembly or looking in the GAC.

To access Excel data, you have to be aware of the hierarchy of things.  At the top is the application class that contains one or more workbooks that contain one or more worksheets.  Within the worksheet are ranges.  Each layer can access down to some of the other layers.

001
002
003
004
005
006
007
008
009
Function Open-ExcelApplication {
Param([switch] $Visible,[switch] $HideAlerts)
    $app = New-Object Microsoft.Office.Interop.Excel.ApplicationClass
    $app.Visible  = $Visible
    $app.DisplayAlerts = -not $HideAlerts
    return $app
}
$app = open-excelApplication -Visible
$app | gm active*

Yields:

Name                      MemberType Definition                                              
----                      ---------- ----------                                              
ActiveCell                Property   Microsoft.Office.Interop.Excel.Range ActiveCell {get;}  
ActiveChart               Property   Microsoft.Office.Interop.Excel.Chart ActiveChart {get;}  
ActiveDialog              Property   Microsoft.Office.Interop.Excel.DialogSheet ActiveDialog ...
ActiveEncryptionSession   Property   int ActiveEncryptionSession {get;}                      
ActiveMenuBar             Property   Microsoft.Office.Interop.Excel.MenuBar ActiveMenuBar {get;}
ActivePrinter             Property   string ActivePrinter {get;set;}                          
ActiveProtectedViewWindow Property   Microsoft.Office.Interop.Excel.ProtectedViewWindow Activ...
ActiveSheet               Property   System.Object ActiveSheet {get;}                        
ActiveWindow              Property   Microsoft.Office.Interop.Excel.Window ActiveWindow {get;}
ActiveWorkbook            Property   Microsoft.Office.Interop.Excel.Workbook ActiveWorkbook {...


All of the classes also have a .Application property that points back to the top.


001
002
003
004
005
006
007
008
009
010
011
012
013
function New-ExcelWorkBook {
Param([parameter(ValueFromPipeline=$true)] $ExcelApplication
,[switch] $Visible)
process {
if ($ExcelApplication -eq $null ) { 
$ExcelApplication  = Open-ExcelApplication -Visible:$Visible
}
$WorkBook = $ExcelApplication.WorkBooks.Add()
return $WorkBook
}
}
$book = $app |  New-ExcelWorkBook
$book | gm active*

Yields:

   TypeName: System.__ComObject#{000208da-0000-0000-c000-000000000046}

Name         MemberType Definition                  
----         ---------- ----------                  
ActiveChart  Property   Chart ActiveChart () {get}  
ActiveSheet  Property   IDispatch ActiveSheet () {get}
ActiveSlicer Property   Slicer ActiveSlicer () {get}

Alternately, you can open an existing workbook:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
function Get-ExcelWorkBook {
Param([parameter(Mandatory=$true,ValueFromPipeline=$true)] $inputObject
,[switch] $Visible
,[switch] $readonly)
[Microsoft.Office.Interop.Excel.ApplicationClass] $app = $null
if ($inputObject -is [Microsoft.Office.Interop.Excel.ApplicationClass]) {
  $app = $inputObject
  $WorkBook = $app.ActiveWorkbook
else {
  $app = Open-ExcelApplication -Visible:$Visible 
  try {
    if ($inputObject.Contains("\\"-or $inputObject.Contains("//")) {
      $WorkBook = $app.Workbooks.Open($inputObject,$true,[System.Boolean]$readonly)
    } else {
      $WorkBook = $app.Workbooks.Open((Resolve-path $inputObject),$true,[System.Boolean]$readonly)
  }} catch {$WorkBook = $app.Workbooks.Open((Resolve-path $inputObject),$true,[System.Boolean]$readonly)}
}
#todo: Add Switch to toggle Full Rebuild (this does an update data)
$app.CalculateFullRebuild()
return $WorkBook
}


The Interop allows you easy access to the classes and enumerations.  The largest caveat is what you may expect  vs what you get when you look at the COM collections.  These collections are built implementing default properties that do not come across in powershell.  A recorded macro may reference WorkSheets("Sheet1") but in PS you will need to say $WorkSheets.item("Sheet1").  So, what looks like it may be an array may need a call to the item property to do what you expect.

When you look at Excel you see cells, when you automate it you have ranges.

001
002
$Sheet = $Book.Worksheets.item("Sheet1")
$sheet | gm -MemberType *Property | where { $_.Definition -match "Range" }

Yields:

  TypeName: System.__ComObject#{000208d8-0000-0000-c000-000000000046}

 Name              MemberType            Definition                        
 ----              ----------            ----------                        
 Range             ParameterizedProperty Range Range (Variant, Variant) {get}
 Cells               Property              Range Cells () {get}              
 CircularReference Property              Range CircularReference () {get}  
 Columns         Property              Range Columns () {get}            
 Rows              Property              Range Rows () {get}                
 UsedRange     Property              Range UsedRange () {get}


 All of the following are the same:

001
002
003
004
005
006
007
$sheet.Range("A1").Text
$sheet.Range("A1:A1").Text
$sheet.Range("A1","A1").Text
$sheet.cells.Item(1,1).text
$sheet.Columns.Item(1).Rows.Item(1).Text
$sheet.Rows.Item(1).Columns.Item(1).Text
$sheet.UsedRange.Range("a1").Text


If you were going to use a formula in a cell, this same convention is used for the .Range ParameterizedProperty.  Cells you have to use the .Item property but you can more easily use in loops as the cell is a coordinate.  UsedRange is limited to the cells that have or have had data in them as a block.  So furthest X and furthest Y make up the range.  If you want to know what these bounds are:

001
002
$sheet.UsedRange.Columns.Count
$sheet.UsedRange.Rows.Count


To close it all down you can do something like:
001
002
003
004
005
006
007
008
009
010
011
012
Function Close-ExcelApplication {
Param([parameter(Mandatory=$true,ValueFromPipeline=$true)] $inputObject)
if ($inputObject -is [Microsoft.Office.Interop.Excel.ApplicationClass]) {
$app = $inputObject 
else {
$app = $inputObject.Application
Release-Ref $inputObject
}
$app.ActiveWorkBook.Close($false| Out-Null
$app.Quit() | Out-Null
Release-Ref $app
}

This will work fine if you only have one workbook open with changes.  If you have more than one open you will need to make sure that you close or save each sheet.  If you try to close and have more than the active workbook not saved, excel will prompt you to save.  This is not something that you want if you expect your script to run unattended.

Another big point to consider is the garbage collection.  I know it was a big concern with Office 2003 and may be unneeded in 2007 or 2010, but an extra step to clean up your variables should be used.

001
002
003
004
005
function Release-Ref ($ref) {
([System.Runtime.InteropServices.Marshal]::ReleaseComObject([System.__ComObject]$ref-gt 0 | Out-Null
[System.GC]::Collect() | Out-Null
[System.GC]::WaitForPendingFinalizers() | Out-Null
}

If you don't do this, Excel may (may)  fail to close.


More later...

Wednesday, March 28, 2012

7z SFX without the temp Config File

I prefer to not use temp files unless I absolutely need to so I replaced

001
002
003
$SetupConfig | out-File "$path\$ConfigName" -enc UTF8
get-content $SFX,$ConfigName,$ZipName -Enc Byte -Read 512 | set-content $OutName -Enc Byte

with:

001
002
003
004
005
$SFXBytes = [io.file]::ReadAllBytes("$PAth\$SFX")
$ConfigBytes = [text.encoding]::ascii.GetBytes($SetupConfig)
$ZipBytes = [io.file]::ReadAllBytes("$PAth\$ZipName")
($SFXBytes,$ConfigBytes,$ZipBytes| set-content $OutName -Enc Byte

I would like to have 7z build the zip to STDOUT but I don't think I can have it go to the 7z format.  Will continue to look at it.

This was an update to http://import-powershell.blogspot.com/2012/03/powershell-7zip-sfx-installer.html

PowerShell 7zip SFX Installer


  1. Save the below two scripts 
  2. Download 7zsd_All_x64.sfx
  3. 7z the 3 files together as 7z.7z, or at a minimum zip Setup.ps1 as 7z.7z
  4. Run 7zInstaller.ps1
  5. It will build a New.EXE then run the EXE.
  6. This will extract the files to a temp folder, and call a powerShell prompt that will run the Setup.PS1 and leave the prompt open.

Modify the Setup.ps1, remove the -NoExit argument, and you can use the script to move the files from the temp location to anywhere you need it.  When the powershell script finishes, the temp folder should be removed.

You can also specify an "InstallPath="path_to_extract"" in the SetupConfig block.  If you do this you don't have to manually copy the files and 7z will not delete the files.

Take a look at http://7zsfx.info/en/parameters.html it has a good collection of what options can be used in the SetupConfig block.  Of note, you can use environment variables, so you could extract directly to a user profile if you wanted to.

7zInstaller.ps1

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041

# Script will not create the 7z file.
$ZipName= "7z.7z"
# Output SFX EXE
$OutName= "new.exe"

# This config data tells the SFX how you want to do things.
# Other than the initial popup, we push all the other options into the PS1.
# http://7zsfx.info/en/parameters.html
$SetupConfig = @"
;!@Install@!UTF-8!
Title="Title Spot"
BeginPrompt="Do you want to install now?"
RunProgram="powershell -NoExit -noprofile -executionpolicy unrestricted -file %%T\\Setup.PS1"
;!@InstallEnd@!
"@


# get from http://7zsfx.info/en/ this was pulled from the "7zSD extra" option.
# There are a selection of these that may display a console vs winforms UI, and
# may also switch between 32bit and 64bit extraction.
$SFX="7zsd_All_x64.sfx" #"7z.SFX"


# this name can be whatever. It will overwrite each time.
$ConfigName= "SetupConfig.txt"

# Need to Move PWD to scripting folder (This simplifies the below, everthing should work with full paths)
$path = $script:MyInvocation.MyCommand.Path
cd $path

$SetupConfig | out-File "$(Split-Path $path)\$ConfigName" -enc UTF8


# The first sample on Binary merges used ADD-Content. This is why I had to kill the file.
# Set-Content though in the same pipeline will append all of the data so we are good.
# The test-path and cleanup is just something nice to keep around.
if (Test-Path $OutName) { del $OutName }
get-content $SFX,$ConfigName,$ZipName -Enc Byte -Read 512 | set-content $OutName -Enc Byte

#Test the installer
Start-process $OutName


Setup.ps1:

001
002
003
Write-Host "Installer Script"
$script:MyInvocation.MyCommand | fl
ls env:7z*Folder*


Posted an update @ http://import-powershell.blogspot.com/2012/03/7z-sfx-without-temp-config-file.html


Wednesday, March 21, 2012

Prize Drawing

We did a simple script with the attendance list piped to a Get-Random to pull winners for the SIG last week.
The problem was for multiple prizes the list didn't get smaller.

PS Script to HTML for Blog


I needed something other than Blogger's QUOTE for script.

Try 1:
If (-not (Test-Path "$home\Documents1\WindowsPowerShell\Highlight-Syntax 2.0.ps1"))
{
    throw "You Need to get `'Highlight-Syntax 2.0.ps1`' from http://poshcode.org/1498`n`n"
}

$psISE.CurrentPowerShellTab.AddOnsMenu.SubMenus.Add("Out-Highlight",{
    if ($psise.CurrentFile.Editor.SelectedText) {
        $source  = $psise.CurrentFile.Editor.SelectedText
    } else {
        $source  =$psise.CurrentFile.Editor.Text
    }
    (& "$home\Documents\WindowsPowerShell\Highlight-Syntax 2.0.ps1" $source $true) |
          Out-Clipboard
},$null) | out-null


Now this worked, then I kept reading and found a Lee Holmes example that was much further along.

Try 2:
Get script from http://www.leeholmes.com/blog/2009/02/03/more-powershell-syntax-highlighting/ I had to fix some single quotes in the $tokenColours definition from copying off the site and the download wasn't clean either.

Around line 203 you find:

001
002
003
004
005
006
if (-not $psise.CurrentOpenedFile)
{
    Write-Error 'No script is available for copying.�
    return
}     
$text = $psise.CurrentOpenedFile.Editor.Text


From the try one modify this to:

001
002
003
004
005
006
007
008
009
010
if (-not $psise.CurrentFile)
{
    Write-Error 'No script is available for copying.�
    return
}
if ($psise.CurrentFile.Editor.SelectedText) {
    $text = $psise.CurrentFile.Editor.SelectedText
else {
    $text = $psise.CurrentFile.Editor.Text
}

...so I can pull a selection.

Now, I can copy with color to WordPad, but still not to MSWord, or the iframe that Blogger uses to compose in.  In Word I get the plain text and in Blogger I get nothing.

With:

001
[System.Windows.Clipboard]::GetDataObject().GetFormats()

The Script produces:

HTML Format
UnicodeText
System.String
Rich Text Format
and copy from Blogger produces:
Text
UnicodeText
System.String
HTML Format
Locale
OEMText
So I added the following.

001
002
$dataObject.SetText([string]$text, [Windows.TextDataFormat]::Text)
$dataObject.SetData([System.Windows.DataFormats]::OemText,[object]$text)

Still no go!

Local is a byte[4].  I assume it is EN_US or something similar to show localization.

I did a copy from each method then ran:

001
[System.Windows.Clipboard]::GetText([System.Windows.TextDataFormat]::Html)


There is some variation in building the "CF_HTML" header.  I think part of the problem may be formatting issues from making a copy of the script off the HTML, with converted quotes.

Reading this code and the associated blog, I understand how the HTML works much better now, but I also learned that PSCX Set-Clipboard has arguments to pupulate the HTML and RTF portions of the clipboard.  So that really solves my problem.  Unfortunately the solution is ignoring the problem.  But I don't see a need to reinvent the wheel.  The down side is that Set-Clipboard doesn't let you stack clipboard formats.  So I can specify RTF, Text, or HTML.  a bummer really.

Called Via an ISE Menu:

001
002
003
$psISE.CurrentPowerShellTab.AddOnsMenu.SubMenus.Add("Copy Color",{
    (& "$home\Documents\WindowsPowerShell\Set-ClipboardScript.ps1")
},$null| out-null