I've created and script with Powershell that copy the result of a dataTable into the clipboard in order to paste it in Excel:
$excelArray = New-Object 'object[,]' $dt_table1.Rows.Count, $dt_table1.Columns.Count
$excelArray = ForEach($Row in $dt1.Tables[0].Rows)
{
$Record = New-Object PSObject
ForEach($Col in $dt1.Tables[0].Columns.ColumnName)
{
Add-Member -InputObject $Record -NotePropertyName $Col -NotePropertyValue $Row.$Col
}
$Record
}
$excelArray | ConvertTo-CSV -NoType -Del "`t" | Select -Skip 1 | Clip
[void]$WorkSheet.columns.Item(1).cells.Item(2).PasteSpecial()
$WorkSheet.Visible = $false
When i Exceute the script using "Powershell ISE" the script works fine, the problem came when i schedule this script using a CMD batch from the "Windows Task Scheduler". When the admin user is connected to the RDP and this batch is executed trough the scheduler it does not copy the result into the excel sheet, it copy the last thing i have in the clipboard.
When executing the batch and closing the RDP session, the batch fails without giving me an error..... What is happening? Maybe there is another way to copy the result of a datatable or dataset into an xcel sheet to avoid the clipboard?
Thanks!
A better way for working with Excel files is by using the module ImportExcel. It uses the EPPLUS library so you don't have to use any COM-objects, install Excel on a server or use the clipboard.
One way of exporting data to an Excel file is simply using Export-Excel.
First you need to install the module from the PowerShell Gallery:
Install-Module ImportExcel
Then you can use it in your scripts:
# myScript.ps1
#Requires -Module ImportExcel
Param (
$Path = 'C:\path\to\the.xlsx'
)
$exportToExcel = ForEach ($Row in $dt1.Tables[0].Rows) {
$properties = #{}
ForEach ($Col in $dt1.Tables[0].Columns.ColumnName) {
$properties[$Col] = $Row.$Col
}
[PSCustomObject]$properties
}
$params = #{
Path = $Path
WorksheetName = 'Data'
TableName = 'Data'
FreezeTopRow = $true
# HideSheet = 'Data'
}
$exportToExcel | Export-Excel #params
Some helpful commands to get you started:
Get-Help Export-Excel # displays what a function, its parameters, ..
Get-Help Export-Excel -Examples # displays how to use the function
Get-Command -Module ImportExcel # list all available functions in a module
Related
I'm working on a project where I need to take a text file and make it an excel file. So far what I've come up with is this.
cls
Remove-Item -path D:\Users\zabin\OneDrive\Desktop\ITS3410\WEEK8\MainWarehouse.csv
Add-Content -path D:\Users\zabin\OneDrive\Desktop\ITS3410\WEEK8\MainWarehouse.csv -Value '"Part_Number","Cost","Price"'
$csvPath = 'D:\Users\zabin\OneDrive\Desktop\ITS3410\WEEK8\MainWarehouse.csv'
#region Excel Test
If (test-path HKLM:SOFTWARE\Classes\Word.Application) {
Write-host "Microsoft Excel installed"
} else {
Write-host "Microsoft Excel not installed"
}
#endregion
#region Patterns
$mainpattern1 = '(?<Partnumber>\d*\s*\w*,)(?<Cost>\d*.\d*),(?<Price>\d*.\d*)'
$mainpattern2 = '(?<Part_number>\d*-\d*-\d*),(?<Cost>\d*.\d*),(?<Price>\d*.\d*)'
#endregion
get-Content 'D:\Users\zabin\OneDrive\Desktop\ITS3410\WEEK8\MainWarehouse.csv' | #grabs the content
Select-String -Pattern $mainpattern1, $mainpattern2 | #selects the patterns
Foreach-Object {
$Part_Number, $Cost, $Price = $_.Matches[0].Groups['Part_number', 'Cost','Price']
[PSCustomObject] #{
part_number = $Part_Number
Cost = $Cost
Price = $Price
}
$objResults | Export-Csv -Path $csvPath -NoTypeInformation -Append
}
some sample data is here
00001143 SP,136.41,227.35
00001223 SP,48.66,81.10
00001236 SP,149.72,249.53
0001-0003-00,100.95,168.25
00015172 W,85.32,142.20
I'm getting the file created and the header values are correct but I'm not sure how to get the values to import.
Continuing from my comment... with the resources and a simple example.
Find-Module -Name '*excel*' | Format-Table -AutoSize
# Results
<#
Version Name Repository Description
------- ---- ---------- -----------
7.1.1 ImportExcel PSGallery PowerShell module to import/export Excel spreadsheets, without Excel....
0.1.12 PSWriteExcel PSGallery Little project to create Excel files without Microsoft Excel being installed.
1.0.2 PSExcel PSGallery Work with Excel without installing Excel
...
0.6.9 ExcelPSLib PSGallery Allow simple creation and manipulation of XLSX file
2.1 Read-ExcelFile PSGallery PowerShell module to import Excel spreadsheets, without Excel....
...
#>
MSExcel will read properly formatted CSV's natively. So, to convert to a true XLS file, use PowerShell, MSOffice COM to open MSExcel with the CSV file, then save it as an XLS format.
$FileName = "$env:temp\Report"
Get-Process |
Export-Csv -UseCulture -Path "$FileName.csv" -NoTypeInformation -Encoding UTF8
$excel = New-Object -ComObject Excel.Application
$excel.Visible = $true
$excel.Workbooks.Open("$FileName.csv").SaveAs("$FileName.xlsx",51)
$excel.Quit()
explorer.exe "/Select,$FileName.xlsx"
Your use case is of course as noted:
Import-Csv -Path 'D:\temp\book1.txt' -header Title, Author
Then using COM as noted above.
I ended up solving this after I asked the question I had alot of flaws in this code
Add-Content -path D:\Users\zabin\OneDrive\Desktop\ITS3410\WEEK8\MainWarehouse.csv -Value '"Part_Number","Cost","Price"'
$csvPath = 'D:\Users\zabin\OneDrive\Desktop\ITS3410\WEEK8\MainWarehouse.csv'
#region Excel Test
If (test-path HKLM:SOFTWARE\Classes\Excel.Application) {#these next few lines will check if excel is installed on the system
Write-host "Microsoft Excel installed"
} else {
Write-host "Microsoft Excel not installed"
}
#endregion
#region Patterns
$mainpattern1 = '(?<Part_number>\d*\s*\w*),(?<Cost>\d*.\d*),(?<Price>\d*.\d*)'#These two line will use REGEX to help seperate the data
$mainpattern2 = '(?<Part_number>\d*-\d*-\d*),(?<Cost>\d*.\d*),(?<Price>\d*.\d*)'
#endregion
get-Content 'D:\Users\zabin\OneDrive\Desktop\ITS3410\WEEK8\Main.rtf' | #grabs the content
Select-String -Pattern $mainpattern2, $mainpattern1 | #selects the patterns
Foreach-Object {
$Part_number, $Cost, $Price = $_.Matches[0].Groups['Part_number', 'Cost','Price'] #Gets the groups of a call to select-string
$results = [PSCustomObject] #{#the list here is what i use to seperate the data onto the CSV file
part_number = $Part_Number
Cost = $Cost
Price = $Price
}
$results | Export-Csv -Path $csvPath -NoTypeInformation -Append #moves the results to the CSV file
}
Im use Powershell module "ImportExcel" for generating xlsx report.
Some string, as "openstack-9cea5509-ed06-4301-b9e2-10f742e8c174:8", add in cell as HyperLink.
I cant remove HyperLink, next method not work:
$d = #(
[PSCustomObject]#{
HLUNID = "String11`nString22"
HostID = "String33`nStrong44"
}
[PSCustomObject]#{
HLUNID = "String55`nString552"
HostID = "openstack-9cea5509-ed06-4301-b9e2-10f742e8c174:8"
}
)
Export-Excel -Path "out1.xlsx" -InputObject $d -WorksheetName 'Luns' -AutoNameRange
$Excel = Open-ExcelPackage -Path "out1.xlsx"
$Excel.Workbook.Worksheets['Luns'].Cells['HostID'] | Set-ExcelRange -ResetFont
$Excel.Workbook.Worksheets['Luns'].Cells[3,2].HyperLink = ''
Close-ExcelPackage -ExcelPackage $Excel
String steel stay clickable:
Image - "Clickable cell"
Next method is not exist:
$Excel.Workbook.Worksheets['Luns'].Cells[3,2].HyperLink.Remove()
$Excel.Workbook.Worksheets['Luns'].Cells[3,2].HyperLink.Dispose()
Please, give me another method for delete HyperLynk.
Try setting the hyperlink $null
$Excel.Workbook.Worksheets['Luns'].Cells[3,2].HyperLink = $null
It will remove the hyperlink, but won't touch the formatting.
Have a read about the method and the functions to manage hyperlink I have proposed to be included into the module.
How to make Excel hyperlinks with PowerShell?
My goal is to add a version number to the file properties of an Excel file that can then be read externally with PowerShell.
If I run (Get-Item "example.xls").VersionInfo I get blank ProductVersion and FileVersion.
ProductVersion FileVersion FileName
-------------- ----------- --------
example.xls
I cannot find a way to set these attributes from VBA. I did find a way to get\set a Revision Number:
Public Function FileVersion() As String
With ThisWorkbook.BuiltinDocumentProperties
FileVersion = .Item("Revision Number").Value
End With
End Function
Public Sub UpdateFileVersion()
With ThisWorkbook.BuiltinDocumentProperties
.Item("Revision Number").Value = .Item("Revision Number").Value + 1
End With
End Sub
However, I can't find a way to read the Revision Number from PowerShell. I either need to read Revision Number from PowerShell or I need to set ProductVersion and FileVersion from VBA. I would accept any combination of things that results in setting a file version in Excel that is visible outside of Excel, ideally I would like to be able to use all of these properties.
You can see the Revision Number I am trying to get from PowerShell and also the Version Number that I cannot set from VBA here:
If you right-click a file and hit properties in the Details tab, you see all that is available.
If you don't want to have to COM into the EOM (Excel Object Model), then you need to assign these in the EOM first, then hit them via PowerShell just as Windows Explorer shows them or enum metadata.
So, something like...
### Get file properties
##
Get-ItemProperty -Path 'D:\Temp' -filter '*.xl*' |
Format-list -Property * -Force
Or
### Enumerate file properties in PowerShell
# get the first file
(
$Path = ($FileName = (Get-ChildItem -Path 'D:\Temp' -Filter '*.xl*').FullName ) |
Select-Object -First 1
)
$shell = New-Object -COMObject Shell.Application
$folder = Split-Path $path
$file = Split-Path $path -Leaf
$shellfolder = $shell.Namespace($folder)
($shellfile = $shellfolder.ParseName($file))
<#
You'll need to know what the ID of the extended attribute is.
This will show you all of the ID's:
#>
0..287 |
Foreach-Object { '{0} = {1}' -f $_, $shellfolder.GetDetailsOf($null, $_) }
# Once you find the one you want you can access it like this:
$shellfolder.GetDetailsOf($shellfile, 216)
As for this...
Thanks but your list, and the one I got from running this on my Excel
file, do not contain Revision
... try it this way.
Gleened from here:
Hey, Scripting Guy! How Can I List All the Properties of a Microsoft
Word Document?
and here:
# Getting specific properties fomr MS Word
$Path = "D:\Temp"
$ObjectProperties = "Author","Keywords","Revision number"
$Application = New-Object -ComObject Word.Application
$Application.Visible = $false
$Binding = "System.Reflection.BindingFlags" -as [type]
$Select = "Name","Created"
$Select += $ObjectProperties
ForEach ($File in (Get-ChildItem $Path -Include '*.docx' -Recurse))
{ $Document = $Application.Documents.Open($File.Fullname)
$Properties = $Document.BuiltInDocumentProperties
$Hash = #{}
$Hash.Add("Name",$File.FullName)
$Hash.Add("Created",$File.CreationTime)
ForEach ($Property in $ObjectProperties)
{ $DocProperties = [System.__ComObject].InvokeMember("item",$Binding::GetProperty,$null,$Properties,$Property)
Try {$Value = [System.__ComObject].InvokeMember("value",$binding::GetProperty,$null,$DocProperties,$null)}
Catch {$Value = $null}
$Hash.Add($Property,$Value)
}
$Document.Close()
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($Properties) |
Out-Null
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($Document) |
Out-Null
New-Object PSObject -Property $Hash |
Select $Select
}
$Application.Quit()
# Results
<#
Name : D:\Temp\Test.docx
Created : 06-Feb-20 14:23:55
Author : ...
Keywords :
Revision number : 5
#>
# Getting specific properties fomr MS Excel
$Path = "D:\Temp"
$ObjectProperties = "Author","Keywords","Revision number"
$Application = New-Object -ComObject excel.Application
$Application.Visible = $false
$Binding = "System.Reflection.BindingFlags" -as [type]
$Select = "Name","Created"
$Select += $ObjectProperties
ForEach ($File in (Get-ChildItem $Path -Include '*.xlsx' -Recurse))
{ $Document = $Application.Workbooks.Open($File.Fullname)
$Properties = $Document.BuiltInDocumentProperties
$Hash = #{}
$Hash.Add("Name",$File.FullName)
$Hash.Add("Created",$File.CreationTime)
ForEach ($Property in $ObjectProperties)
{ $DocProperties = [System.__ComObject].InvokeMember("item",$Binding::GetProperty,$null,$Properties,$Property)
Try {$Value = [System.__ComObject].InvokeMember("value",$binding::GetProperty,$null,$DocProperties,$null)}
Catch {$Value = $null}
$Hash.Add($Property,$Value)
}
$Document.Close()
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($Properties) |
Out-Null
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($Document) |
Out-Null
New-Object PSObject -Property $Hash |
Select $Select
}
$Application.Quit()
# Results
<#
Name : D:\Temp\Test.xlsx
Created : 25-Nov-19 20:47:15
Author : ...
Keywords :
Revision number : 2
#>
Point of note: I meant to add sources:
Regarding setting properties, see this Word example from the MS PowerShellgallery.com, which can be tweaked of course for other Office docs.
Set specific word document properties using PowerShell
The attached script uses the Word automation model to set a specific
BuiltIn Word document property. It is provided as an example of how to
do this. You will need to modify the pattern used to find the files,
as well as the built-in Word property and value you wish to assign.
As note above, getting is the same thing...
Get Word built-in document properties
This script will allow you to specify specific Word built-in document
properties. It returns an object containing the specified word
document properties as well as the path to those documents. Because a
PowerShell object returns, you can filter and search different
information fr
Thanks to #postanote for pointing me in the right direction. None of the code offered worked out of the box for me.
This is what I ended up doing to pull the Revision Number from my Excel document:
<# Get-Excel-Property.ps1 v1.0.0 by Adam Kauffman 2020-02-03
Returns the property value from an Excel File
#>
param(
[Parameter(Mandatory=$true, Position=0)][string]$FilePath,
[Parameter(Mandatory=$true, Position=1)][string]$ObjectProperties
)
Function Get-Property-Value {
[CmdletBinding()]Param (
[Parameter(Mandatory = $true)]$ComObject,
[Parameter(Mandatory = $true)][String]$Property
)
$Binding = "System.Reflection.BindingFlags" -as [type]
Try {
$ObjectType = $ComObject.GetType()
$Item = $ObjectType.InvokeMember("Item",$Binding::GetProperty,$null,$ComObject,$Property)
return $ObjectType.InvokeMember("Value",$Binding::GetProperty,$null,$Item,$null)
}
Catch {
return $null
}
}
# Main
$Application = New-Object -ComObject Excel.Application
$Application.Visible = $false
$Document = $Application.Workbooks.Open($FilePath)
$Properties = $Document.BuiltInDocumentProperties
$Hash = #{}
$Hash.Add("Name",$FilePath)
ForEach ($Property in $ObjectProperties)
{
$Value = Get-Property-Value -ComObject $Properties -Property $Property
$Hash.Add($Property,$Value)
}
# COM Object Cleanup
if ($null -ne $Document) {
$Document.Close($false)
Remove-Variable -Name Document
}
if ($null -ne $Properties) {
Remove-Variable -Name Properties
}
if ($null -ne $Application) {
$Application.Quit()
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($Application) | Out-Null
Remove-Variable -Name Application
}
[gc]::collect()
[gc]::WaitForPendingFinalizers()
# Show collected information
New-Object PSObject -Property $Hash
I am working on a script for PowerShell to import data from 2 separate Excel sheets, compare all properties of these sheets and export a new Excel worksheet with this output.
The purpose of this script is to compare an updated excel sheet with a previous version and spot what has changed, what has been added and possibly even what has been removed.
I have been using the Import-Excel module to handle the first part, and Compare-Object for the data comparison.
As far as I can tell Import-Excel imports the Excel data into a System.Object which is a hashtable of PSCustomObjects. Each PSCustomObject corresponds to a row in the Excel sheet.
I am running the following code:
$global:InputdataA = Import-Excel -Path $InputA -WorkSheetname $InputSheetA
$global:InputdataB = Import-Excel -Path $InputB -WorkSheetname $InputSheetB
$global:ReferenceObject = $InputdataA
$global:DifferenceObject = $InputdataB
$global:InputdataHeadersA = $InputdataA[0].psobject.properties.name
$global:InputdataHeadersB = $InputdataB[0].psobject.properties.name
$props = $InputdataHeadersA
$props += $InputdataHeadersB
$props = $props | Select -Unique
$compareResult = Compare-Object -ReferenceObject $ReferenceObject -DifferenceObject $DifferenceObject -Property $props -PassThru -CaseSensitive
I am using 2 excel files for testing:
Personnel_16_12_2018 - small2.xlsx (previous version)
Personnel_28_11_2018 - small2.xlsx (new version with changes and additions)
The files can be downloaded here:
https://ufile.io/bmstu
https://ufile.io/3z62x
The output I would like to see would be only the 7 entries that have been changed/added, with data from difference object (the new version of the excel sheet) This would represent the latest and "correct" data.
Currently I get an output from the compare-object, containing the 7 entries AND the 6 entries that have been changed from the reference object including side-indicator.
Is it possible to make the compare-object return only the changes or do I have to process the output afterwards?
Based on your additional information and the sample files, you could try something like this:
$oldFile = Import-Excel ".\personnel_28_11_2018---small2.xlsx"
$newFile = Import-Excel ".\personnel_16_12_2018---small2.xlsx"
$properties = "TRIAL_PK", "TRIALCOUNTRY_PK", "TRIALSSITE_PK", "ASSIGNMENT_LVL", "ROLE", "INT_EXT", "START_DATA", "END_DATE", "PERSONNELL_PK", "TITLE", "LAST_NAME", "FIRST_NAME", "ORGANIZATION_NAME"
$result = Compare-Object -ReferenceObject $oldFile -DifferenceObject $newFile -Property $properties -PassThru -CaseSensitive | Where-Object {$_.SideIndicator -eq "=>"}
$result | Select-Object $properties | Export-Excel ".\changed.xlsx"
I have this PowerShell code that loops through Excel files in a specified directory; references a list of known passwords to find the correct one; and then opens, decrypts, and saves that file to a new directory.
But it's not executing as quickly as I'd like (it's part of a larger ETL process and it's a bottleneck). At this point I can remove the passwords faster manually as the script takes ~40 minutes to decrypt 40 workbooks while referencing a list of ~50 passwords.
Is there a cmdlet or function (or something) that's missing which would speed this up, an overlooked flaw in the processing, or is PowerShell, perhaps, just not the right tool for this job?
Original Code (updated code can be found below):
$ErrorActionPreference = "SilentlyContinue"
CLS
# Paths
$encrypted_path = "C:\PoShTest\Encrypted\"
$decrypted_Path = "C:\PoShTest\Decrypted\"
$original_Path = "C:\PoShTest\Originals\"
$password_Path = "C:\PoShTest\Passwords\Passwords.txt"
# Load Password Cache
$arrPasswords = Get-Content -Path $password_Path
# Load File List
$arrFiles = Get-ChildItem $encrypted_path
# Create counter to display progress
[int] $count = ($arrfiles.count -1)
# Loop through each file
$arrFiles| % {
$file = get-item -path $_.fullname
# Display current file
write-host "Processing" $file.name -f "DarkYellow"
write-host "Items remaining: " $count `n
# Excel xlsx
if ($file.Extension -eq ".xlsx") {
# Loop through password cache
$arrPasswords | % {
$passwd = $_
# New Excel Object
$ExcelObj = $null
$ExcelObj = New-Object -ComObject Excel.Application
$ExcelObj.Visible = $false
# Attempt to open file
$Workbook = $ExcelObj.Workbooks.Open($file.fullname,1,$false,5,$passwd)
$Workbook.Activate()
# if password is correct - Save new file without password to $decrypted_Path
if ($Workbook.Worksheets.count -ne 0) {
$Workbook.Password=$null
$savePath = $decrypted_Path+$file.Name
write-host "Decrypted: " $file.Name -f "DarkGreen"
$Workbook.SaveAs($savePath)
# Close document and Application
$ExcelObj.Workbooks.close()
$ExcelObj.Application.Quit()
# Move original file to $original_Path
move-item $file.fullname -Destination $original_Path -Force
}
else {
# Close document and Application
write-host "PASSWORD NOT FOUND: " $file.name -f "Magenta"
$ExcelObj.Close()
$ExcelObj.Application.Quit()
}
}
}
$count--
# Next File
}
Write-host "`n Processing Complete" -f "Green"
Updated code:
# Get Current EXCEL Process ID's so they are not affected but the scripts cleanup
# SilentlyContinue in case there are no active Excels
$currentExcelProcessIDs = (Get-Process excel -ErrorAction SilentlyContinue).Id
$a = Get-Date
$ErrorActionPreference = "SilentlyContinue"
CLS
# Paths
$encrypted_path = "C:\PoShTest\Encrypted"
$decrypted_Path = "C:\PoShTest\Decrypted\"
$processed_Path = "C:\PoShTest\Processed\"
$password_Path = "C:\PoShTest\Passwords\Passwords.txt"
# Load Password Cache
$arrPasswords = Get-Content -Path $password_Path
# Load File List
$arrFiles = Get-ChildItem $encrypted_path
# Create counter to display progress
[int] $count = ($arrfiles.count -1)
# New Excel Object
$ExcelObj = $null
$ExcelObj = New-Object -ComObject Excel.Application
$ExcelObj.Visible = $false
# Loop through each file
$arrFiles| % {
$file = get-item -path $_.fullname
# Display current file
write-host "`n Processing" $file.name -f "DarkYellow"
write-host "`n Items remaining: " $count `n
# Excel xlsx
if ($file.Extension -like "*.xls*") {
# Loop through password cache
$arrPasswords | % {
$passwd = $_
# Attempt to open file
$Workbook = $ExcelObj.Workbooks.Open($file.fullname,1,$false,5,$passwd)
$Workbook.Activate()
# if password is correct, remove $passwd from array and save new file without password to $decrypted_Path
if ($Workbook.Worksheets.count -ne 0)
{
$Workbook.Password=$null
$savePath = $decrypted_Path+$file.Name
write-host "Decrypted: " $file.Name -f "DarkGreen"
$Workbook.SaveAs($savePath)
# Added to keep Excel process memory utilization in check
$ExcelObj.Workbooks.close()
# Move original file to $processed_Path
move-item $file.fullname -Destination $processed_Path -Force
}
else {
# Close Document
$ExcelObj.Workbooks.Close()
}
}
}
$count--
# Next File
}
# Close Document and Application
$ExcelObj.Workbooks.close()
$ExcelObj.Application.Quit()
Write-host "`nProcessing Complete!" -f "Green"
Write-host "`nFiles w/o a matching password can be found in the Encrypted folder."
Write-host "`nTime Started : " $a.ToShortTimeString()
Write-host "Time Completed : " $(Get-Date).ToShortTimeString()
Write-host "`nTotal Duration : "
NEW-TIMESPAN –Start $a –End $(Get-Date)
# Remove any stale Excel processes created by this script's execution
Get-Process excel -ErrorAction SilentlyContinue | Where-Object{$currentExcelProcessIDs -notcontains $_.id} | Stop-Process
If nothing else I do see one glaring performance issue that should be easy to address. You are opening a new excel instance for testing each individual password for each document. 40 workbooks with 50 passwords mean you have opened 2000 Excel instances one at a time.
You should be able to keep using the same one without a functionality hit. Get this code out of your inner most loop
# New Excel Object
$ExcelObj = $null
$ExcelObj = New-Object -ComObject Excel.Application
$ExcelObj.Visible = $false
as well as the snippet that would close the process. It would need to be out of the loop as well.
$ExcelObj.Close()
$ExcelObj.Application.Quit()
If that does not help enough you would have to consider doing some sort of parallel processing with jobs etc. I have a basic solution in a CodeReview.SE answer of mine doing something similar.
Basically what it does is run several excels at once where each one works on a chunk of documents which runs faster than one Excel doing them all. Just like I do in the linked answer I caution the automation of Excel COM with PowerShell. COM objects don't always get released properly and locks can be left on files or processes.
You are looping for all 50 passwords regardless of success or not. That means you could find the right password on the first go but you are still going to try the other 49! Set a flag in the loop to break that inner loop when that happens.
As far as the password logic goes you say that
At this point I can remove the passwords faster manually since the script takes ~40 minutes
Why can you do it faster? What do you know that the script does not. I don't see you being able to out perform the script but doing exactly what it does.
With what I see another suggestion would be to keep/track successful passwords and associated file name. So that way when it gets processed again you would know the first password to try.
This solution uses the modules ImportExcel for easier working with Excel files, and PoshRSJob for multithreaded processing.
If you do not have these, install them by running:
Install-Module ImportExcel -scope CurrentUser
Install-Module PoshRSJob -scope CurrentUser
I've raised an issue on the ImportExcel module GitHub page where I've proposed a solution to open encrypted Excel files. The author may propose a better solution (and consider the impact across other functions in the module, but this works for me). For now, you'll need to make a modification to the Import-Excel function yourself:
Open: C:\Username\Documents\WindowsPowerShell\Modules\ImportExcel\2.4.0\ImportExcel.psm1 and scroll to the Import-Excel function. Replace:
[switch]$DataOnly
With
[switch]$DataOnly,
[String]$Password
Then replace the following line:
$xl = New-Object -TypeName OfficeOpenXml.ExcelPackage -ArgumentList $stream
With the code suggested here. This will let you call the Import-Excel function with a -Password parameter.
Next we need our function to repeatedly try and open a singular Excel file using a known set of passwords. Open a PowerShell window and paste in the following function (note: this function has a default output path defined, and also outputs passwords in the verbose stream - make sure no-one is looking over your shoulder or just remove that if you'd prefer):
function Remove-ExcelEncryption
{
[CmdletBinding()]
Param
(
[Parameter(Mandatory=$true)]
[String]
$File,
[Parameter(Mandatory=$false)]
[String]
$OutputPath = 'C:\PoShTest\Decrypted',
[Parameter(Mandatory=$true)]
[Array]
$PasswordArray
)
$filename = Split-Path -Path $file -Leaf
foreach($Password in $PasswordArray)
{
Write-Verbose "Attempting to open $file with password: $Password"
try
{
$ExcelData = Import-Excel -path $file -Password $Password -ErrorAction Stop
Write-Verbose "Successfully opened file."
}
catch
{
Write-Verbose "Failed with error $($Error[0].Exception.Message)"
continue
}
try
{
$null = $ExcelData | Export-Excel -Path $OutputPath\$filename
return "Success"
}
catch
{
Write-Warning "Could not save to $OutputPath\$filename"
}
}
}
Finally, we can run code to do the work:
$Start = get-date
$PasswordArray = #('dj7F9vsm','kDZq737b','wrzCgTWk','DqP2KtZ4')
$files = Get-ChildItem -Path 'C:\PoShTest\Encrypted'
$files | Start-RSJob -Name {$_.Name} -ScriptBlock {
Remove-ExcelEncryption -File $_.Fullname -PasswordArray $Using:PasswordArray -Verbose
} -FunctionsToLoad Remove-ExcelEncryption -ModulesToImport Import-Excel | Wait-RSJob | Receive-RSJob
$end = Get-Date
New-TimeSpan -Start $Start -End $end
For me, if the correct password is first in the list it runs in 13 seconds against 128 Excel files. If I call the function in a standard foreach loop, it takes 27 seconds.
To view which files were successfully converted we can inspect the output property on the RSJob objects (this is the output of the Remove-ExcelEncryption function where I've told it to return "Success"):
Get-RSJob | Select-Object -Property Name,Output
Hope that helps.