How to execute powershell script from Excel? - excel

I have fully functional poweshell script, but when I try to run it from MS Excel 2010 I got an error message "You cannot call a method on a null-valued expression" and "cannot index into a null array".
I don't know where is a problem, because as I mentioned the script works without any issues when I don't try to execute it from Excel.
Thank you for any suggestions.
$paths = Get-ChildItem 'E:\TEMP' -Filter '*.txt'
$delete = Get-Content 'E:\TEMP\TEMP1\delete.log'
ForEach ($path in $paths) {
$pathtmp = "$path.tmp"
$sr = New-Object -TypeName System.IO.StreamReader -ArgumentList $path
$sw = New-Object -TypeName System.IO.StreamWriter -ArgumentList $pathtmp
Do {
$line = $sr.ReadLine()
$Column = $line.split(",")
If ($delete -notcontains $Column[1]) {
$sw.WriteLine($line)
}
} Until ( $sr.EndOfStream )
$sr.close()
$sw.close()
Remove-Item $path
Rename-Item $pathtmp $path
}
This script will delete entire row from all text files in a directory if first column of text file matches string from delete.log file.

An issue of the script is that it normally executes only if it being run inside of the directory where the script is placed itself, i think.
In this situation it would be e:\temp\temp1.
This possibly was the reason why excel complains, because working directories are already being set to other places than the script.
If to modify it slightly, it is possible to make it working from anywhere.
One of the possible solutions may be with assigning a full working path to read and write files variables.
Try to use these scripts:
test.ps1 script (inside e:\temp\temp1)
$paths = Get-ChildItem 'e:\TEMP' -Filter '*.txt'
$delete = Get-Content 'e:\TEMP\TEMP1\delete.log'
ForEach ($path in $paths) {
$fpath = $path.fullname
$pathtmp = "$fpath.txt"
$sr = New-Object -TypeName System.IO.StreamReader -ArgumentList "$fpath"
$sw = New-Object -TypeName System.IO.StreamWriter -ArgumentList "$pathtmp"
Do {
$line = $sr.ReadLine()
$Column = $line.split(",")
If ($delete -notcontains $Column[0]) {
$sw.WriteLine($line)
}
} Until ( $sr.EndOfStream )
$sr.close()
$sw.close()
Remove-Item "$fpath"
Rename-Item "$pathtmp" "$fpath"
}
excel macro
Sub test()
Call Shell(Environ$("COMSPEC") & " /c powershell -file E:\temp\temp1\test.ps1", vbNormalFocus)
End Sub

Related

Powershell code cannot recognize all excel file(*.xlsm) in current location

I'm currently working on Powershell to handle excel files(*.xlsm).
The problem is the code below can only read "test.xlsm".
When the name is not test like "this.xlsm" , that code cannot read the file.
Any help...?
Thanks for your answer in advance :)
$destination = "C:\JJ\"
$dirName = Get-ChildItem -Name -Filter *.xlsm
$saveAs = $destination + "new\"
foreach($z in $dirName){
$excel=New-Object -ComObject Excel.Application
$excel.visible=$false
$excel.Displ`ayAlerts=$false
$book=$excel.Workbooks.Open($destination + $z)
$sheet=$book.Worksheets.item(1)
$sheet.Cells.Item(1,5)="=max(B2:B6)"
$book.SaveAs($saveAs + $z)
$excel.Quit()
$excel=$null
}
You are using confusing variable names in your code.. Why call the source path $destination??
Anyway, you should use the -File switch on your Get-ChildItem cmdlet to make sure this will only return FileInfo objects, not DirectoryInfo objects aswell. (these are Objects, not strings)
Then, there is a better way to construct paths. Use Join-Path instead of concatenating things like with $destination + $z.
Lastly, I would create the Excel object only once, before the loop and cleanup memory afterwards. Now, you are creating new COM objects in every iteration and never release them from memory.
Below code should do what you intend:
$source = "C:\JJ"
$destination = Join-Path -Path $source -ChildPath 'new'
# test if the destination path already exists and if not, create it
if (!(Test-Path -Path $destination -PathType Container)) {
$null = New-Item -Path $destination -ItemType Directory
}
# create the Excel COM object outside the loop
$excel = New-Object -ComObject Excel.Application
$excel.visible = $false
$excel.DisplayAlerts = $false
# get all *.xlsm files inside the source folder
Get-ChildItem -Path $source -Filter '*.xlsm' -File | ForEach-Object {
# The $_ automatic variable represents 1 FileInfo object in each iteration
$book = $excel.Workbooks.Open($_.FullName)
$sheet = $book.Worksheets.item(1)
$sheet.Cells.Item(1,5) = "=max(B2:B6)"
# join the destination path and the file name for output
$saveAs = Join-Path -Path $destination -ChildPath $_.Name
$book.SaveAs($saveAs)
$book.Close()
}
# cleanup Com objects
$excel.Quit()
$null = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($sheet)
$null = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($book)
$null = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($excel)
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
$excel = $null
Note, If you have PowerShell version below 3.0, the -File switch is not available. Instead, then use
Get-ChildItem -Path $source -Filter '*.xlsm' | Where-Object {!$_.PSIsContainer} | ForEach-Object { ... }
# remove -name to get a directory object instead of a string
$dirName = Get-ChildItem -Filter *.xlsm
# you need the file name from the directory object listing
foreach($z in $dirName.name){ ...... }

Excel.Application: Microsoft Excel cannot access the file '[<filename>]' There are several possible reasons:

I have a PowerShell script that works, it helps me run multiple queries against multiple servers and save each output in different CSV and then merge them together into an Excel file.
$Servers = get-content -Path "Servers.txt"
$DatabaseName ="master"
#$credential = Get-Credential #Prompt for user credentials
$secpasswd = ConvertTo-SecureString "MyPassword" -AsPlainText -Force
$credential = New-Object System.Management.Automation.PSCredential ("sa", $secpasswd)
$QueriesFolder = "Queries\"
$ResultFolder = "Results\"
ForEach($Server in $Servers)
{
$DateTime = (Get-Date).tostring("yyyy-MM-dd")
ForEach ($filename in get-childitem -path $QueriesFolder -filter "*.sql" | sort-object {if (($i = $_.BaseName -as [int])) {$i} else {$_}} )
{
$oresults = invoke-sqlcmd -ServerInstance $Server -Database $DatabaseName -Credential $credential -InputFile $filename.fullname
write-host "Executing $filename on $Server"
$BaseNameOnly = Get-Item $filename.fullname | Select-Object -ExpandProperty BaseName
$oresults | export-csv $ResultFolder$BaseNameOnly.csv -NoTypeInformation -Force
}
$All_CSVs = get-childitem -path $ResultFolder -filter "*.csv" | sort-object {if (($i = $_.BaseName -as [int])) {$i} else {$_}}
$Count_CSVs = $All_CSVs.Count
Write-Host "Detected the following CSV files: ($Count_CSVs)"
Write-Host " "$All_CSVs.Name"`n"
$ExcelApp = New-Object -ComObject Excel.Application
$ExcelApp.SheetsInNewWorkbook = $All_CSVs.Count
$output = "C:\Users\FrancescoM\Desktop\CSV\Results\" + $Server + " $DateTime.xlsx"
if (Test-Path $output)
{
Remove-Item $output
Write-Host Removing: $output because it exists already
}
$xlsx = $ExcelApp.Workbooks.Add()
for($i=1;$i -le $Count_CSVs;$i++)
{
$worksheet = $xlsx.Worksheets.Item($i)
$worksheet.Name = $All_CSVs[$i-1].Name
$file = (Import-Csv $All_CSVs[$i-1].FullName)
$file | ConvertTo-Csv -Delimiter "`t" -NoTypeInformation | Clip
$worksheet.Cells.Item(1).PasteSpecial()|out-null
}
$xlsx.SaveAs($output)
Write-Host Creating: $output
$ExcelApp.Quit()
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($xlsx) | Out-Null;
Write-Host "Closing all worksheet"
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($ExcelApp) | Out-Null;
Write-Host "Closing Excel"
[System.GC]::Collect();
[System.GC]::WaitForPendingFinalizers()
Remove-Item "$ResultFolder\*" -Include *.csv
Write-Host "Cleaning all *.csv"
Start-Sleep -Seconds 3
}
In order to make this script more portable I want all the paths mentioned into it to be stored into a variable and then concatenated.
But as soon as I change:
$output = "C:\Users\FrancescoM\Desktop\CSV\Results\" + $Server + " $DateTime.xlsx"
into:
$output = $ResultFolder + $Server + " $DateTime.xlsx"
things get nasty and I receive the error:
Microsoft Excel cannot access the file 'C:\Users\FrancescoM\Documents\Results\0DC80000'.
There are several possible reasons:
• The file name or path does not exist.
• The file is being used by another program.
• The workbook you are trying to save has the same name as a currently open workbook.
At C:\Users\FrancescoM\Desktop\CSV\QueryLauncher.ps1:50 char:2
+ $xlsx.SaveAs($output)
+ ~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (:) [], COMException
+ FullyQualifiedErrorId : System.Runtime.InteropServices.COMException
I don't understand, I think I'm concatenating things right.
I also followed this StackOverflow post and restarted my computer after adding "C:\Windows\SysWOW64\config\systemprofile\desktop" but the problem isn't fixed.
How can a variable path mess things up with Excel?
Because you are not defining the full path in the $ResultFolder variable, it will be expanded using the current working directory.
Just look at the path you want it to be:
"C:\Users\FrancescoM\Desktop\CSV\Results\" + $Server + " $DateTime.xlsx"
and the resulting path using the partial $ResultFolder variable:
C:\Users\FrancescoM\Documents\Results\0DC80000
Since you want the output file in a folder on your desktop, set the $output to
$output = Join-Path $([Environment]::GetFolderPath("Desktop")) "CSV\Results\$Server $DateTime.xlsx"
EDIT
From your last comment I understand that you want the output to be in a subfolder called "Results" that resides inside the folder the script itself is in.
In that case do this:
# get the folder this script is running from
$ScriptFolder = if ($PSScriptRoot) { $PSScriptRoot } else { Split-Path $MyInvocation.MyCommand.Path }
# the following paths are relative to the path this script is in
$QueriesFolder = Join-Path -Path $ScriptFolder -ChildPath 'Queries'
$ResultFolder = Join-Path -Path $ScriptFolder -ChildPath 'Results'
# make sure the 'Results' folder exists; create if not
if (!(Test-Path -Path $ResultFolder -PathType Container)) {
New-Item -Path $ResultFolder -ItemType Directory | Out-Null
}
Then, when it becomes time to save the xlsx file, create the full path and filename using:
$output = Join-Path -Path $ResultFolder -ChildPath "$Server $DateTime.xlsx"
$xlsx.SaveAs($output)
P.S. I advice to use the Join-Path cmdlet to combine file paths or to make use of [System.IO.Path]::Combine() instead of joining paths together like you do with this line: $oresults | export-csv $ResultFolder$BaseNameOnly.csv. Using the latter can lead to unforeseen pathnames if ever you forget to postfix the first path part with a backslash.
P.S.2 Excel has its own default output path set in Tools->Options->General->Default File Location and has no idea of the relative path for the script. This is why you should save using a Full path and filename.

Excel spawned from powershell script not quitting [duplicate]

This question already has answers here:
Excel, save and close after run
(6 answers)
Closed 4 years ago.
We're migrating databases so I'm using powershell to modify the hundreds of excel files that reference to old DB instance to the new one. This is all fine and works as intended. The problem I have is that the Excel application will not exit when I'm done. The process will just hang around as a background process and I need to go into task manager to kill it. Not a huge deal but it is annoying. Here is my script. I'm on powershell v5 and Office 2016.
param(
[string]$search_root=$(throw "missing search root parameter"),
[boolean]$test=$true
)
echo $search_root
$NAMEPOSTFIX = '-Updated'
$OLDCONN = 'Data Source=abc;'
$NEWCONN = 'Data Source=xyz;'
$filelist = Get-ChildItem -Path $search_root *.xls* -Recurse -Exclude '*Updated.*'
$Excel = New-Object -Com Excel.Application
$Excel.DisplayAlerts = $False
function update_con_xls{
param($file)
$Workbook = $Excel.Workbooks.Open($file)
foreach($con in $Workbook.Connections){
if ($con.OLEDBConnection -ne $null){
$con.OLEDBConnection.Connection = $con.OLEDBConnection.Connection.Replace($OLDCONN,$NEWCONN)
}
if ($con.ODBCConnection -ne $null){
$con.ODBCConnection.Connection = $con.ODBCConnection.Connection.Replace($OLDCONN,$NEWCONN)
}
}
$Workbook.Save()
$Workbook.saved = $true
$Excel.Workbooks.Close()
}
foreach ($file in $filelist) {
echo $file
if ($file.Extension -eq '.xls' -or $file.Extension -eq '.xlsx') {
$newfile = ($file.DirectoryName + '\' + $file.BaseName + $NAMEPOSTFIX + $file.Extension)
if($test){
echo $test
$newfile = ('C:\test\' + $file.Name) #for test run to copy coppy locally
}
Copy-Item $file.FullName -Destination $newfile
update_con_xls($newfile)
}
}
$Excel.Quit()
$Excel = $null
Maybe change:
$Excel.Workbooks.Close()
to :
$Excel= New-Object -ComObject Excel.Application;
$Workbook = $Excel.Workbooks.Open($file);
$Workbook.Save();
$Workbook.Close(); # <--- try
$Excel.Quit();
Remove-Variable -Name Excel;
I do not see anything here that could close your ODB and OLEDb connection trying to add:
$con.OLEDBConnection.Connection.Close();
$con.ODBCConnection.Connection.Close();
or
$con.OLEDBConnection.Close();
$con.ODBCConnection.Close();
after your work is finished.

Powershell script stops working when ran through task scheduler

I am running the script below as a scheduled task with the user logged on the server. It converts an xls file to csv using the Excel.Application COM object. The conversion works, but eventually breaks and I don't know why.
I have the task run the following command which should in theory allow it to run constantly:
powershell.exe -noexit -file "filename.ps1"
Any thoughts on what to try?
$server = "\\server"
$xls = "\path\XLS\"
$csv = "\path\CSV\"
$folder = $server + $xls
$destination = $server + $csv
$filter = "*.xls" # <-- set this according to your requirements
$fsw = New-Object IO.FileSystemWatcher $folder, $filter -Property #{
IncludeSubdirectories = $true # <-- set this according to your requirements
NotifyFilter = [IO.NotifyFilters]"FileName, LastWrite"
}
Register-ObjectEvent $fsw Created -SourceIdentifier FileCreated -Action {
$path = $Event.SourceEventArgs.FullPath
$name = $Event.SourceEventArgs.Name
$changeType = $Event.SourceEventArgs.ChangeType
$timeStamp = $Event.TimeGenerated
$excelFile = $folder + $name
$E = New-Object -ComObject Excel.Application
$E.Visible = $false
$E.DisplayAlerts = $false
$wb = $E.Workbooks.Open($excelFile)
foreach ($ws in $wb.Worksheets) {
$n = "output_" + $name -replace ".XLS"
$ws.SaveAs($destination + $n + ".csv", 6)
}
$E.Quit()
}
I was doing something similar with word. I couldnt use Quit alone. I think using Quit hides Excel. Try releasing the com object by using:
$E.Quit()
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($E)
Remove-Variable E
i dont know if you are opening the Excel application but if you are you can maybe use
$wb.close($false)
Let me know if it works...
Ref: https://technet.microsoft.com/en-us/library/ff730962.aspx?f=255&MSPPError=-2147217396

Using Parameters in Powershell to assign folder locations

I'm new to powershell and I need some help here. Below is a script I wrote to locate an excel file in folder. The files in the excel sheet would be compared to the contents of another folder on the same machine. Locations are : "C:\MKS_DEV\" and The resultant matched files would be zipped and put in another location as shown in the scripts. These scripts would be used by other people on different machines so the locations of both folders could differ on different machines.
I want to write an argument or using parameters for the location of both folders so that I wouldn't have to specify the location all the time I have to run the scripts and cant figure out how to implement this.
The scripts works perfectly but I just need to incorporate arguments/parameters into it. Any help would be very much appreciated.
Thanks.
Here is the code:
# Creating an object for the Excel COM addin
$ExcelObject = New-Object -ComObject Excel.Application
# Opening the Workbook
$ExcelWorkbook = $ExcelObject.Workbooks.Open("C:\Eric_Source\Test.xls")
# Opening the Worksheet by using the index (1 for the first worksheet)
$ExcelWorksheet = $ExcelWorkbook.Worksheets.Item(1)
# The folder where the files will be copied/The folder which will be zipped
# later
$a = Get-Date
$targetfolder = "C:\"+$a.Day+$a.Month+$a.Year+$a.Hour+$a.Minute+$a.Second
# Check if the folder already exists. Command Test-Path $targetfolder returns
# true or false.
if(Test-Path $targetfolder)
{
# delete the folder if it already exists. The following command deletes a
# particular directory
Remove-Item $targetfolder -Force -Recurse -ErrorAction SilentlyContinue
}
# The following command is used to create a particular directory
New-Item -ItemType directory -Path $targetfolder
# Declaration of variables, COlumn value = 6 for Column F
$row = 1
$col = 6
# Read a value from the worksheet with the following command
$filename = $ExcelWorksheet.Cells.Item($row,$col).Value2
$filename
# change the folder value below to specify the folder where the powershell
# needs to search for the filename that it reads from excel file.
$folder = "C:\MKS_DEV\"
$null = ""
You have multiple ways to parameter your script.
The first one is to us $args[n] [automatic variable]1.
If your script is called MyScript.PS1 you can call it with :
MyScript.PS1 "C:\Eric_Source\Test.xls"
Then inside your script use $args[0] for the first argument.
Another way is to use the reserved word Param at the begining of your script:
Param ($MyParam1, $MyParam2)
When you call your script $MyParam1 will contain the first param and so on.
You could create it as a function and load it.
Function Folder-Deletion ($ExcelWorkbook,$targetfolder) {
$ExcelObject = New-Object -ComObject Excel.Application
$ExcelOpen = $ExcelObject.Workbooks.Open($ExcelWorkbook)
$ExcelWorksheet = $ExcelOpen.Worksheets.Item(1)
$a = Get-Date
if(Test-Path $targetfolder)
{
Remove-Item $targetfolder -Force -Recurse -ErrorAction SilentlyContinue
}
New-Item -ItemType directory -Path $targetfolder
$row = 1
$col = 6
$filename = $ExcelWorksheet.Cells.Item($row,$col).Value2
$filename
}
Then run the function against the spreadsheet and folder like so:
Folder-Deletion C:\Eric_Source\Test.xls C:\MKS_DEV
Or you could create a PowerShell script file (e.g. FolderDeletion.ps1) with the following contents:
param($ExcelWorkbook,$targetfolder)
$ExcelObject = New-Object -ComObject Excel.Application
$ExcelOpen = $ExcelObject.Workbooks.Open($ExcelWorkbook)
$ExcelWorksheet = $ExcelOpen.Worksheets.Item(1)
$a = Get-Date
if(Test-Path $targetfolder)
{
Remove-Item $targetfolder -Force -Recurse -ErrorAction SilentlyContinue
}
New-Item -ItemType directory -Path $targetfolder
$row = 1
$col = 6
$filename = $ExcelWorksheet.Cells.Item($row,$col).Value2
$filename
Then run the script against the spreadsheet and folder like so:
FolderDeletion.ps1 C:\Eric_Source\Test.xls C:\MKS_DEV

Resources