powershell: if excel already running, get instance, otherwise start it - with exception handling - excel

Using Powershell, I want to import some tab-separated Ascii files into MS Excel. I use a loop for doing so, and right now I have a simple solution that works:
for each file: start Excel , import tsv file, close Excel.
..assuming Excel is in the Path, it's the right version of Excel, Excel 2010
Now I want to switch to a more efficient version: keep excel open.
for each file: grab running instance of excel if there is one, if not, try to start excel. Process file. Keep excel open. At the end, keep it open ( I want to look at the excel files while the script is running, which could take a while. Annoyingly, in the current version of the script excel is being closed while I am looking at the output).
I haven't found a comprehensive solution for this, neither here nor elsewhere on the internet. With "comprehensive" I mean "Exception Handling". In Powershell, it's a bit confusing. There are two ways of dealing with exceptions: using trap and a try-catch block.
Here is my code, thrown together from several internet sources , how can I improve it?
I want to wrap it in a function, but COM objects as return values are problematic. (
What I want is a combination of "simple factory" and "singleton".)
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Office.Interop.Excel")
try {
$excelApp = [System.Runtime.InteropServices.Marshal]::GetActiveObject("Excel.Application")
} catch [System.Runtime.InteropServices.COMException], [System.Management.Automation.RuntimeException]{
write-host
write-host $("TRAPPED: " + $_.Exception.GetType().FullName);
write-host $("TRAPPED: " + $_.Exception.Message);
write-host "Excel is not running, trying to start it";
$excelApp = New-Object -ComObject "Excel.Application"
if (-not $excelApp){
# excel not installed, not in path
Write-Error "Excel not running, and cannot be started, exiting."
# Todo: test if excel version is correct, e.g. english Excel 2007 or 2010., if not set outfile extension xls.
exit;
}
}
catch [System.Exception]{
write-host $("EXCEPTION: " + $_.Exception.GetType().FullName);
write-host $("EXCEPTION: " + $_.Exception.Message);c
Write-Error 'Something went wrong during creation of "Excel.Application" object, => Exit.'
exit;
}

It's been a while since you asked this, but I have run into the same problem. Your code appears to be only good example of a reasonable solution. I do question your concern, however. Specifically, once Excel is running is there a need to call this routine again in a function? That is, once Excel is open, then you can treat it as a service to open/close workbooks, access worksheets within them, and eventually $excelApp.Quit() the application (although in Powershell v1.0 the .Quit() won't exit the app... but that is OK since I am grabbing a running instance anyways).
The link below discusses a way of starting up and grabbing the PID of Excel instance in order to explicitly kill it if needed.
Discussion on Quiting/Killing Excel from within Powershell

All variables must be null. Excel.Workbooks - all open book.
Check count of Workbooks. If count = 0, quit. After close of Powershell Excel will be close too.
$Workbook.Close()
$WorkSheet = $null;
$Workbook = $null;
If($excelApp.Workbooks.Count -eq 0)
{$excelApp.Quit()}
$excelApp = $null;

Related

How to open an Excel file directly from a Powershell script?

I'm using Powershell version 7.2.1
When I run the command below on Powershell, it works fine, i.e. Excel is launched and the target Excel file is opened.
& 'PATH_TO_EXCEL_EXE' 'PATH_TO_TARGET_FILE'
Howewer, when I pass this command to a scheduled task, Excel is launched but the target file isn't opened. More details about my Powershell script:
$action = New-ScheduledTaskAction -Execute "& 'PATH_TO_EXCEL_EXE' 'PATH_TO_TARGET_FILE'"
...
Register-ScheduledTask -Action $action -Trigger $trigger -TaskName "myTask"
# The line below is for testing purpose, to see if my scheduled task can launch correctly.
Start-ScheduledTask -TaskName "myTask"
Can someone tell me the reason why Powershell fails to replicate the action, when it's passed to a scheduled task as above? Any workarounds/solutions would be appreciated.
Based on ZivkoK's comment, I'm adding an answer which solves the issue:
$excelExePath = ...
$excelFile = ...
$action = New-ScheduledTaskAction -Execute $excelExePath -Argument """$excelFile"""
A scheduled task based on the action above does what it intends to do.
P.S. Triple quotes surrounding excelFile parameter are needed when the excel file path contains space characters. For example, if the excel file path is "C:\MyUser\some random file.xlsx", then Excel tries to open 3 files named "some", "random", and "file.xlsx". Using triple quotes solves this problem.

What is the best practice to capture an open Excel document as a COM Object (not creating a new one) on Windows using PowerShell

I try to automate my workflow on my day-to-day job. Since Excel is frequently used. Using PowerShell for Excel automation is crucial for me.
Most of the time I do have my Excel windows open. Thus I use below code block to capture these windows for further automation:
$FullPath = $FilePath + "\" + $WorkBookName
$excel = [Runtime.InteropServices.Marshal]::GetActiveObject("Excel.Application")
#capture already active object
if (($excel.Workbooks | ? {$_.Fullname}).FullName -eq $FullPath) {
$wb = $excel.workbooks | ?{$_.FullName -eq $FullPath}
} else {
# if no document is available open from filepath
$excel, $wb = Open-NewCliExcel -TypeOrPath $FullPath
}
I'm pretty sure that there must be more stable and reliable practices in terms of scalability. My plan is to build an intra-company library to start the automation of other people's processes.

Issue with Powershell Excel attempting to cast .csv Values

At this point, I believe it may be a file I/O issue.
While utilizing a Powershell script invoking Excel methods to go through a .csv file from a website, powershell is attempting to cast placeholders for data that is too long for a cell "#######" instead of the date and time contained within the 'cell' (search engines may need 'pound sign' or 'hashtag' to reach this result).
Below is the offending portion of the script.
[DateTime]$S = $sheet.Cells.Item($rowS+$i,$colS).text
[DateTime]$G = $sheet.Cells.Item($rowG+$i,$colG).text
[DateTime]$A = $sheet.Cells.Item($rowA+$i,$colSWScan).text
The data should exist as MM/DD/YYYY HH:MM, but is being read by Powershell/PSExcelModule as #######, which is what is displayed with the Excel GUI when opening the file.
This is only a portion of what the entire script does. Any suggestions on how to resolve the error while maintaining usage of PSExcel-Module would be most helpful.
Stackoverflow seems to have an issue with me posting the verbose error message, and this is my first post. Let me know if that would be helfpul with troubleshooting.
Edit for comment #1:
# Create an instance of Excel.Application and Open Excel file
$objExcel = New-Object -ComObject Excel.Application
# Open the file
$workbook = $objExcel.Workbooks.Open($file)
# Activate the first worksheet
$sheet = $workbook.Worksheets.Item($sheetName)
$objExcel.Visible=$false
After getting my head out of 'Excelland', I realized it may be easier to re-write the script to utilize the .csv organization (the original imported file for the script was a .xlsx), but I am admittedly unfamiliar with .csv scripting. However, the original question still stands while I re-write the code as I may need to switch back to .xlsx imported documents. Thank you for the suggestion J E Carter II.
Answer:
$objExcel.Cells.EntireColumn.AutoFit()
Credit to J E Carter II
When you open an excel file as an object under windows, you're launching excel.
You might be able to add the following commands to your excel object handle to get the data to represent correctly.
$objExcel.Cells.Select
$objExcel.Cells.EntireColumn.AutoFit
Do this before getting values from cells. If that doesn't work, let me know and I can find some csv handling examples.
Those might work better on the $workbook object. I can't remember which is implicit when recording a macro (which is how I got those).
It's also possible you may need to precede those two lines with something like
$workbook.Sheets("sheetname").Select

How can I stop Excel processes from running in the background after a PowerShell script?

No matter what I try, Excel 2013 continues to run in the background on Windows 10 no matter what commands I throw at the end of my PowerShell script. I've tried adding all suggestions I've found to the end of my script and the only Excel object I open continues to remain open. Here is what I have at the end of my script. Any other suggestions?
## Quit Excel and Terminate Excel Application process:
$xlsxwb.quit
$xlsxobj.Quit
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($xlsxobj)
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($xlsxwb)
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($xlsxSh1)
Start-Sleep 1
'Excel processes: {0}' -f #(Get-Process excel -ea 0).Count
I ran into the same problem and tried various solutions without success. I got closer when I started releasing all of the COM objects I saved as variables, not just the ones for the workbook, worksheet, and Excel application.
For example, take the following example code:
$Excel = New-Object -ComObject Excel.Application
$Excel.Visible = $False
$Workbook = $Excel.Workbooks.Open("C:\Temp\test.xlsx")
$Worksheet = $Workbook.Worksheets.Item(1)
$UsedRange = $Worksheet.UsedRange
$Range = $Worksheet.Range("A1:B10")
$Workbook.Close()
$Excel.Quit()
[void][System.Runtime.Interopservices.Marshal]::ReleaseComObject($Range)
[void][System.Runtime.Interopservices.Marshal]::ReleaseComObject($UsedRange)
[void][System.Runtime.Interopservices.Marshal]::ReleaseComObject($Worksheet)
[void][System.Runtime.Interopservices.Marshal]::ReleaseComObject($Workbook)
[void][System.Runtime.Interopservices.Marshal]::ReleaseComObject($Excel)
[GC]::Collect()
If you were to take out just one of the ReleaseComObject statements, the Excel process would remain open. In my code I release all the ones like ranges, tables, etc. first and then I do the worksheet, workbook, and finally the Excel application itself. Then because that only seemed to work like 90% of the time, I added the garbage collection command at the end and finally had a solution that seems to work every time without having to kill the process.
Note: My system is Windows 8.1 with PowerShell v5 and Office 2013.
Here's a simple example below. It will likely require some additional code for more complex procedures.
function _FullQuit {
while ( $this.Workbooks.Count -gt 0 ) {
$this.Workbooks.Item(1).Close()
}
$this.Quit()
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($this)
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
}
function New-Excel {
$object = New-Object -ComObject "Excel.Application"
$object | Add-Member -MemberType ScriptMethod -Name FullQuit -Value {_FullQuit}
$object
}
$xl = New-Excel
$wb1 = $xl.Workbooks.Open("C:\Data1.csv")
$wb2 = $xl.Workbooks.Open("C:\Data2.csv")
$xl.FullQuit()
Create Excel Application.
Make it visible
Get Process Id of the application.
Hide Excel Application.
Stop process my process id.
Sample Code
# Create Excel Application
$excel = New-Object -comobject Excel.Application
# Make it visiable
$excel.Visible = $true
# Get Windows handle of the application
$excelWinHwnd = $excel.Hwnd
# Get Process Id of the application
$process = Get-Process Excel | Where-Object {$_.MainWindowHandle -eq $excelWinHwnd}
$excelProcessId = $process.Id
# Hide the application : Run In background
$excel.Visible = $false
# Kill/Stop the process by id
Stop-Process -Id $excelProcessId
The above solutions did not work for me, in the sequence I needed the final step was .saveas(file.xlsx) which meant that the remaining unsaved document still popped a gui interface requiring user interaction to save/don't save/cancel.
I ended up with the following, which is admittedly rough, but worked for me.
At the beginning of the script:
$existingExcel = #()
Get-Process Excel | % {$existingExcel += $_.ID }
function Stop-Excel
{
Get-process EXCEL | % {IF($_.ID -notmatch $existingExcel){Stop-Process -ID $_.ID}}
}
and at the end of the script
Stop-Excel
This has the advantage of completely destroying any lingering excel processes without terminating any other live excel processes that may be in use by the users that run have to this script.
The disadvantages are that when you next load excel you are presented with a crashed excel document recovery dialogue.
Alternative answer:
I understand that my reply is late, still, consider following approach to get this done.
At the beginning, get the PIDs of any/all instances of excel.exe, using command:
tasklist /FI "imagename eq excel.exe"
Run the part of script that generates its instance of excel.exe. Use the command in step 1 to identify and save PID of newly generated instance of excel.exe (say wxyz), this will be different from already existing PIDs saved in step 1.
At the end of script, close the specific instance (with PID saved in step 2), using command:
TASKKILL /f /PID wxyz
where wxyz is 4 digit PID saved in step 2.
If nothing else works reliably, try to make the Excel application's DisplayAlerts to tru just before quitting.
$xlapp.DisplayAlerts=$true
$xlapp.quit()
[System.Runtime.Interopservices.Marshal]::ReleaseComObject([System.__ComObject]$xlapp)
$xlapp=$null
remove-variable xlapp
[GC]::Collect()
This always works for me.

Why is perl saving two copies of Excel spreadsheet?

This is similar to A copy of Excel Addin is created in My Documents after saving, except that I'm working with Perl instead of VBA, and xls files instead of xlsm, and the negative impact of the behavior is different.
I've inherited a Perl script (Perl 5.8.8) that is running on Windows 2003 Server as SYSTEM. After copying an Excel 2003 template file to a unique, fully defined path location, it opens the unique file in Excel using OLE, edits the file, saves the file, and closes the file. What results is the edited file being saved both in the correct, fully-defined path location, and also in the Default User profile's Documents folder.
This causes thousands of these files to accumulate on the C: drive, as every new admin to be hired gets a copy in his Documents folder.
Adding the code that sets the value of $OUT:
if (!$db->Sql("EXEC GetDetails 'name'"))
{
while ($db->FetchRow()>0)
{
#DataIn = $db->Data();
$name = $DataIn[0];
$IN = $DataIn[1];
$OUT = $DataIn[2];
opendir(DIR,"$OUT") || die "$OUT directory does not exist $!\n";
#... loop of proprietary code
#...
#Completed = $db1->Data();
#...
&formatExcelReport #The code that I previously posted
#...
# more proprietary code
# end of loop
} #end of while
}#end of if
The code I originally posted:
# Initialize Excel object
eval {Win32::OLE->new('Excel.Application', 'Quit')};
eval {$Excel = Win32::OLE->GetActiveObject('Excel.Application')};
unless (defined $Excel)
{
$Excel = Win32::OLE->GetActiveObject('Excel.Application')
|| Win32::OLE->new('Excel.Application', 'Quit');
}
$infiles = "Report_Template.xls";
$infiles = $OUT."/".$infiles;
$db6->Sql("EXEC FormatResults '".$Completed[0]."','".$Completed[1]."'");
$row = 2;
$fileName = $Completed[0]."_".$Completed[1];
$uniquefile = $fileName.$printdate.".xls";
# $OUT is a fully defined path on the E: drive
$reportfile = "$OUT"."\\".$uniquefile;
copy($infiles,$reportfile);
$Book = $Excel->Workbooks->Open("$reportfile");
$sheetnum = 1;
my $Sheet = $Book->Worksheets($sheetnum);
# Set Headers
$Header = $Sheet->PageSetup->{'CenterHeader'};
$Header = $Header." Results Test Code: ".$Completed[0]." Worksheet: ".$Completed[1]." Date: ".$headerdate;
$Sheet->PageSetup->{'CenterHeader'}= $Header;
# More file editing
# ...
$Book->Save();
$Book->Close(0);
Win32::OLE->new('Excel.Application', 'Quit');
Is the root of this problem the Save() command? Should I be using SaveAs() instead?
Any other feedback about how Excel is being used welcome, as well.
Thanks!
I don't see what causes this behavior, but here are a few things to try.
The template and the file it is copied to have names
$infiles = $OUT."/".$infiles;
$reportfile = "$OUT"."\\".$uniquefile;
Use the same separator.
Try to suppress some possible setting dictating that another copy be made. Perhaphs
$Excel->Application->{CreateBackup} = 0;
However, this may not be the correct property -- search the VB or Excel documentation for properties that may result in Excel saving an extra copy. (It needn't be "backup".)
Try to create a new file and use SaveAs, as a test to see whether you get two files again. The template copying may be setting it off to Save an extra copy (even though I don't see how). I'd say it's either that, or some general setting that need be turned off.
The rest is the original post, about using SaveAs, whereby I thought that a new file is created
You would use SaveAs to write a new file. See saveas in MSDN library
Saves changes to the workbook in a different file.
Using the save method may result in saving two files fro some reason, as noted in the answer by Borodin. This page also advises to use SaveAs for a new file
The first time you save a workbook, use the SaveAs method to specify a name for the file.
Once you change to using SaveAs there should be a confirmation dialog to deal with. If you want to suppress that you can set a property, with one (or either?) of
$Excel->Application->{DisplayAlerts} = 0;
# or
$Excel->{DisplayAlerts} = 0;
For a number of options, including backups for example, see the Chapter on OLE automation in PERL in a Nutshell.
A note on some other resources. There is a cookbook of sorts in this post on perlmonks. A listing of various operations is given in this SO post.
Finally, I don't know how deep the reasons for using OLE are but if it is only about writing some Excel files there are other modules. For example the very well regarded Spreadsheet::WriteExcel and Excel-Writer-XLSX.
That's very strange Perl code. eval without checking $# afterwards is just wrong -- you need to know if a step of your code has failed for the following steps to make sense
It looks like the problem is in your call to copy($infiles, $reportfile). That will save one copy of the file, while $Book->Save and $Book->Close will save another

Resources