Remove "sharing" from Excel workbook - powershell

I'm trying to save a workbook to a new location with a password and keep the filename the same. The filname gets updated weekly with a date appended to it, so it's never the same. There are two things I'm having trouble with:
Using the SaveAs method to save the file with the same name in a different path &
I can't add a password because the workbook is shared.
I'm scripting this out in PowerShell, and if possible, I'd like to unshare the workbook in the script. Unfortunately, I can't seem to find a method that accomplishes this. Here is what I have so far... I really appreciate any advice.
$xls = new-object -com excel.application
$xls.Visible = $False
$xlsWB = $xls.Workbooks.Open("path\*.xlsx")
$xlsWB.Password = "Password"
$xlsWB.SaveAs("differentPath\*.xlsx")
$xls.Quit()
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($xls)
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($xlsWB)

Turns out, there is a method that can be called to change the sharing status of the workbook. It's ExclusiveAccess().
Here is my working code that solved the problem:
$xls = new-object -comObject excel.application
$xls.Visible = $False
$xls.DisplayAlerts = $False
$xlsWB = $xls.Workbooks.Open("FilePath")
$xlsWB.ExclusiveAccess()
$xlsWB.Password = "AddThisPassword"
$xlsWB.Save()
$xlsWB.Close()
$xls.Quit()
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($xls)
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($xlsWB)
move "CurrentPath" "NewPath"
Once I change the sharing status of the workbook, the Password method successfully adds a password to the workbook, solving the second issue described in the OP. Rather than use SaveAs(), I decided to simply move the file, which saves me from deleting the source file.
Thanks and I hope someone finds this useful.

this was my attempt at a similar problem. It removes the MultiUserEditing of files within a folder structure.
foreach ($file in (Get-ChildItem "C:\path_to_reports\" -File -Filter "*.xls" -recurse))
{
#The filter seems to also work for *.xlxsx
$Excel = New-Object -comobject Excel.Application
$Excel.Visible = $False
$Excel.DisplayAlerts = $False
$ExcelWorkbook = $Excel.workbooks.open($file.fullname)
If ($ExcelWorkbook.MultiUserEditing -eq "True")
{
$ExcelWorkbook.ExclusiveAccess()
$ExcelWorkbook.Save()
}
#close the workbook and not the file
$ExcelWorkbook.Close()
#Quit the file
$Excel.Quit()
#cleanup
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
#more clean up
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($Excel)
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($ExcelWorkbook)
#the most clean up
Remove-Variable -Name excel
}

Related

How Do I Grab Text File Output and Pass Down Pipeline?

I've got an application that opens a winform and asks the user to input a PDF file. Because I can't read strings in PDF files easily, I need to convert it to a .txt. When the user clicks OK, the application does this.
The problem I'm having is now using the .txt file object and passing it to another command without knowing the name of it. When I try to pipe it to another command, it won't work because I don't have the path. I think this is because the output of conversion is the string "OK" and not the actual .txt file.
How can I convert the PDFs to text (I'm using Xpdf) and pass the converted file down the pipeline for further processing?
If the means I'm using is the problem, how can I accomplish this task another way?
Add-Type -AssemblyName System.Windows.Forms
$form = New-Object System.Windows.Forms.Form
$form.StartPosition = 'CenterScreen'
$button = New-Object System.Windows.Forms.Button
$form.Controls.Add($button)
$button.Text = 'Get file'
$button.Location = '10,10'
$button.Add_Click({
$ofd = New-Object system.windows.forms.Openfiledialog
$ofd.Filter = 'PDFs (*.pdf)|*.pdf'
$script:filename = 'Not found'
if ($ofd.ShowDialog() -eq 'Ok') {
$script:filename = $textbox.Text = $ofd.FileName
}
})
$buttonOK = New-Object System.Windows.Forms.Button
$form.Controls.Add($buttonOK)
$buttonOK.Text = 'Ok'
$buttonOK.Location = '10,40'
$buttonOK.DialogResult = 'OK'
$textbox = New-Object System.Windows.Forms.TextBox
$form.Controls.Add($textbox)
$textbox.Location = '100,10'
$textbox.Width += 50
$form.ShowDialog()
$output = & "C:\Users\eakinsa\Desktop\Style Guide Report\Includes\bin32\pdftotext" $filename
$output |
Get-Location -OutVariable textFile |
Select-String -Path $textFile -Pattern ed
Per Ansgar:
I amended the lines last few lines to, for now, maintain the default functionality of pdftotext where it creates the file in the same directory with the same name, as with his suggestion, I could easily replace .pdf with .txt on the end of the file path, thereby having the flexibility to pass the correct file path to subsequent functions. That made it so I was able to search the text file.
& "C:\users\eakinsa\Desktop\Style Guide Report\Includes\bin32\pdftotext" $filename
$pdf = Get-Item $filename
$textfile = $filename -replace '\.pdf$', '.txt'
Select-String -Path $textfile -Pattern ed
When you run pdftotext with just the input PDF as argument it creates the output text file in the same directory with the same basename and the extension txt.
& pdftotext C:\temp\foo.pdf # creates C:\temp\foo.txt
So you can build the text file path like this:
$pdf = Get-Item $filename
$textfile = Join-Path $pdf.DirectoryName ($pdf.BaseName + '.txt')
or like this:
$textfile = $filename -replace '\.pdf$', '.txt'
Alternatively you can tell pdftotext where to create the output file:
$textfile = 'C:\some\where\bar.txt'
& pdftotext $filename $textfile # creates C:\some\where\bar.txt

Powershell script to convert a word table to text

I have this script that is supposed to convert the first table from a word document to text, but unfortunately it does nothing:
$word = New-Object -ComObject word.application
$word.visible = $false
$folderpath = "C:\mypath\myfile.docx"
$Doc = $word.Documents.open($folderpath)
$Doc.Tables.item(1).ConvertToText
$Doc.saveas([ref]$folderpath, [ref]$SaveFormat::wdFormatDocumentDefault)
$Doc.close()
$word.Quit()
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($word)
However using a VB script with essentially the same code does do this seemingly easy task perfectly:
Dim wrdApp: Set wrdApp = WScript.CreateObject(""Word.Application"")
Dim wrdDoc
Set wrdDoc = wrdApp.Documents.Open("C:\mypath\myfile.docx")
wrdDoc.Tables(1).ConvertToText
wrdDoc.SaveAS ("C:\mypath\myfile.docx")
wrdDoc.Close SaveChanges=True
wrdApp.Quit
Set wrdApp = Nothing
Set wrdDoc = Nothing
Can anyone point me towards the reason the powershell script is simply not converting the table?
Thank you!
Tiaan
I have tried the following code and it worked for me. Pretty same as yours. You need to add parenthesis at the end of method convertToText(not required in vbscript) and simply save the document.
$word = New-Object -ComObject word.application
$word.visible = $false
$path = "C:\mypath\myfile.docx"
$Doc = $word.documents.open($path)
$Doc.Tables.item(1).convertToText() # <--- Add parenthesis () at the end
$Doc.save
$Doc.close
$word.quit

Powershell with Word CheckSpelling and selected dictionary

I would like to spell check some strings using the Microsoft Word API in Powershell and a specific dictionary ("English (US)").
I use the following code to do the checking but it does not seem to take into account the dictionary I want. Any ideas what is wrong? Also, command "New-Object -COM Word.Dictionary" seems to fail.
$word = New-Object -COM Word.Application
$dictionary = New-Object -COM Word.Dictionary
foreach ($language in $word.Languages) {
if ($language.Name.Contains("English (US)")) {
$dictionary = $language.ActiveSpellingDictionary;
break;
}
}
Write-Host $dictionary.Name
$check = $word.CheckSpelling("Color", [ref]$null, [ref]$null, [ref]$dictionary)
if(!$check) {
Write-Host "Spelling Error!"
}
$word.quit()
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($word) | Out-Null
Remove-Variable word
The COMobject word.dictionary does not exist (at least not on my machine), here is what worked for me in the short test i did:
$dic = New-Object -COM Scripting.Dictionary #credits to MickyB
$w = New-Object -COM Word.Application
$w.Languages | % {if($_.Name -eq "English (US)"){$dic=$_.ActiveSpellingDictionary}}
$w.checkSpelling("Color", [ref]$null, [ref]$null, [ref]$dic)
Another possibility in addition to Paul's:
$dictionary = New-Object -COM Scripting.Dictionary
I was experiencing the same problem, that the Word.Application.checkSpelling() method seems to ignore any dictionary passed to it. I worked around the issue creating a Word document, defining a text range, changing the LanguageID of this range to the language I want to proof read against, and then inspecting detected spelling errors. Here is the code:
<#Function which helps to pick the language#>
function FindLanguage($language_name){
foreach($element in $Word.Languages){
if($element.Name -eq $language_name){
$element.Name
return $element
}
}
}
$Proofread_text = "The lazie frog jumpss over over the small dog."
$Word = New-Object -COM Word.Application
$Document = $Word.Documents.Add()
$Textrange = $Document.Range(0)
$english = FindLanguage("English (US)")
$Textrange.LanguageID = $english.ID
$Textrange.InsertAfter($Proofread_text)
<#Handle misspelled words here#>
foreach($spell_error in $textrange.SpellingErrors){
Write-Host $spell_error.Text
}
$Document.Close(0)
$Word.Quit()
The output will be:
>>lazie
>>jumpss
>>over
I found it helpful to disable the language autodetection in word before starting the script. Especially when you plan to switch the language.

Powershell popup while file is being moved

When my script starts it moves a file from one directory to another. After the file is completely downloaded I launch an application.
This all works, but what I would like is a popup window to appear while the file is being moved (Large files).
When I debug my code once it hits the Move-Item Cmdlet it waits until that command is completed before it moves on. What I want to do is while the Move-Item Cmdlet is running, popup an information window.
I know how to do the popup and the Move-Item, I just don't know how to get it to work the way I want. Any ideas?
Popup code
#pop up window letting mechanic know we are waiting for the files to be downloaded before opeing the SMT application
$wshell = New-Object -ComObject Wscript.Shell
$wshell.Popup("The EAFR file is still being moved to the correct directory, please wait.",0,"SMT Status",0)
#Move-Item
$MLMoveDir = "C:\move\data\AutoUpload\"
Move-Item -LiteralPath ($filePath) $MLMoveDir
One option is to use WinForms to display a Please Wait dialog, rather than a Popup that has to be dismissed by the user. Something like:
Add-Type -AssemblyName System.Windows.Forms
$Form = New-Object system.Windows.Forms.Form
$Label = New-Object System.Windows.Forms.Label
$Form.Controls.Add($Label)
$Label.Text = "Copying file, please wait."
$Label.AutoSize = $True
$Form.Visible = $True
$Form.Update()
#Move-Item
$MLMoveDir = "C:\move\data\AutoUpload\"
Move-Item -LiteralPath ($filePath) $MLMoveDir
#Hide popup
$Form.Close()
So what you could do is start the Move-Item as a job, and then do a While((get-job "jobname").state -ne completed){do popup}. I would look something like this:
#Move-Item
$MLMoveDir = "C:\move\data\AutoUpload\"
$MoveJob = Start-Job -scriptblock {Move-Item -LiteralPath ($filePath) $MLMoveDir}
#Show Popup
While($movejob.state -ne "Completed"){
$wshell = New-Object -ComObject Wscript.Shell
$wshell.Popup("The EAFR file is still being moved to the correct directory, please wait.",1,"SMT Status",0)
}
That way the popup shows for 1 second, and if the move is still happening it shows it again. I don't know that it would even appear to disappear/re-show so it would likely be seamless.

How to Open; SaveAs; then Close an Excel 2013 (macro-enabled) workbook from PowerShell4

Doing a search on the above Com operations yields links dating to '09 and even earlier. Perhaps it hasn't changed but I find myself bumping up against errors where 'it is being used by another process.' - even though no Excel app is open on my desktop. I have to reboot to resume.
To be clear - I'm trying to open an existing file; immediately SaveAs() (that much works), add a sheet, Save(); Close() - and then, importantly, repeat that cycle. In effect, I'm creating a few dozen new sheets within a loop that executes the above 'Open Master; SaveAs(); Edit Stuff; Save; Close;
From the examples I've seen this is not a typical workflow for PowerShell. Pasted at the very bottom is my provisional script - pretty rough and incomplete but things are opening what they need to open and adding sheet also works - until I know I have the right way to cleanly close stuff out I'm not worried about the iterations.
I've found a couple examples that address closing:
From http://theolddogscriptingblog.wordpress.com/2012/06/07/get-rid-of-the-excel-com-object-once-and-for-all/
$x = New-Object -com Excel.Application
$x.Visible = $True
Start-Sleep 5
$x.Quit()
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($x)
Remove-Variable x
And from http://social.technet.microsoft.com/Forums/windowsserver/en-US/24e57b61-e792-40c1-8aff-b0a8205f48ab/updated-opened-excel-using-powershell?forum=winserverpowershell
Set-ItemProperty $path -name IsReadOnly -value $false
$Excel.ActiveWorkBook.Save()
$openfile.Close()
$openfile = $null
$Excel.Quit()
$Excel = $null
[GC]::Collect()
<>
function MakeNewBook($theWeek, $AffID){
$ExcelFile = "C:\csv\InvoiceTemplate.xlsm"
$Excel = New-Object -Com Excel.Application
$Excel.Visible = $True
$Workbook = $Excel.Workbooks.Open($ExcelFile)
$theWeek = $theWeek -replace "C:\\csv\\", ""
$theWeek = $theWeek -replace "\.csv", ""
$theWeek = "c:\csv\Invoices\" +$AffID +"_" + $theWeek + ".xlsm"
$SummaryWorksheet = $Workbook.worksheets.Item(1)
$Workbook.SaveAs($theWeek)
return $Excel
}
function MakeNewSheet($myBook, $ClassID){
$SheetName = "w"+$ClassID
#$Excel = New-Object -Com Excel.Application
#$Excel.Visible = $True
$wSheet = $myBook.WorkSheets.Add()
}
function SaveSheet ($myExcel)
{
#$WorkBook.EntireColumn.AutoFit()
#Set-ItemProperty $path -name IsReadOnly -value $false
$myExcel.ActiveWorkBook.Save()
$openfile= $myExcel.ActiveWorkBook
$openfile.Close()
$openfile = $null
$myExcel.Quit()
$myExcel = $null
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($myExcel)
Remove-Variable $myExcel
[GC]::Collect()
}
$theWeek = "C:\csv\wkStart2013-11-04.csv"
$x = Import-Csv $theWeek
foreach ($xLine in $x){
if ($x[0]){
$AffID = $x[1].idAffiliate
$myExcel = MakeNewBook $theWeek $x[1].idAffiliate
$ClassID = $x[1].idClass
MakeNewSheet $myExcel $ClassID
continue
}
SaveSheet($myExcel)
$AffID = $_.$AffID
$wID = $xLine.idClass
#MakeNewSheet($wID)
Echo "$wID"
}
As a follow up after playing around with this issue myself. I geared my solution around Ron Thompson's comment minus the function calls:
# collect excel process ids before and after new excel background process is started
$priorExcelProcesses = Get-Process -name "*Excel*" | % { $_.Id }
$Excel = New-Object -ComObject Excel.Application
$postExcelProcesses = Get-Process -name "*Excel*" | % { $_.Id }
# your code here (probably looping through the Excel document in some way
# try to gently quit
$Excel.Quit()
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($Excel)
# otherwise allow stop-process to clean up
$postExcelProcesses | ? { $priorExcelProcesses -eq $null -or $priorExcelProcesses -notcontains $_ } | % { Stop-Process -Id $_ }
My experience has been that the Quit method doesn't work well, especially when looping. When you get the error, instead of rebooting, open up Task Manager and look at the Processes tab. I'm willing to bet you'll see Excel still open -- maybe even multiple instances of it. I solved this problem by using Get-Process to find all instances of Excel and piping them to Stop-Process. Doesn't seem like that should be necessary, but it did the trick for me.
You should not have to keep track of processes and kill them off.
My experience has been that to properly and completely close Excel (including in loops), you also need to release COM references. In my own testing have found removing the variable for Excel also ensures no remaining references exist which will keep Excel.exe open (like if you are debugging in the ISE).
Without performing the above, if you look in Task Manager, you may see Excel still running...in some cases, many copies.
This has to do with how the COM object is wrapped in a “runtime callable wrapper".
Here is the skeleton code that should be used:
$excel = New-Object -ComObject Excel.Application
$excel.Visible = $true
$workbook = $excel.Workbooks.Add()
# or $workbook = $excel.Workbooks.Open($xlsxPath)
# do work with Excel...
$workbook.SaveAs($xlsxPath)
$excel.Quit()
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($excel)
# no $ needed on variable name in Remove-Variable call
Remove-Variable excel
Try this
$filePath = "E:\TSMBackup\TSMDATAPull\ODCLXTSM01_VM.xlsx"
$excelObj = New-Object -ComObject Excel.Application
$excelObj.Visible = $true
$workBook = $excelObj.Workbooks.Open($filePath)
$workSheet = $workBook.Sheets.Item("Sheet1")
$workSheet.Select()
$workBook.RefreshAll()
$workBook.Save()
$excelObj.Quit()