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