I am building a script to write word documents using PowerShell (Windows 7, PS4, .Net 4, Office 2010.
The script works but it embeds the text at the top of the document. I need to be able to specify the exact location like on page 2.
Here's what I got up to date:
# Opens an MsWord Doc, does a Search-and-Replace and embeds a text file as an object
# To make this work you need in the same folder as this script:
# -A 2-page MsWord file called "Search_and_replace_Target_Template.doc" with:
# -the string <package-name> on page ONE
# -the string <TextFile-Placeholder> on page TWO
# -A textfile called "TextFile.txt"
#
#The script will:
# -replace <package-name> with something on page one
# -embed the text file <package-name> at the top of page one (but I want it to replace <TextFile-Placeholder> on page 2)
#
# CAVEAT: Using MsWord 2010
[String]$MsWordDocTemplateName = "Search_and_replace_Target_Template.doc"
[String]$TextFileName = "TextFile.txt"
[Int]$wdReplaceAll = 2
[Int]$wdFindContinue = 1 #The find operation continues when the beginning or end of the search range is reached.
[Bool]$MatchCase = $False
[Bool]$MatchWholeWord = $true
[Bool]$MatchWildcards = $False
[Bool]$MatchSoundsLike = $False
[Bool]$MatchAllWordForms = $False
[Bool]$Forward = $True
[Int]$Wrap = $wdFindContinue #The find operation continues when the beginning or end of the search range is reached.
[Bool]$Format = $False
[Int]$wdReplaceNone = 0
$objWord = New-Object -ComObject Word.Application
$objWord.Visible = $true #Makes the MsWord
[String]$ScriptDirectory = [System.IO.Path]::GetDirectoryName($myInvocation.MyCommand.path)
[String]$WordDocTemplatePath = "$ScriptDirectory\$MsWordDocTemplateName"
[String]$TextFilePath = "$ScriptDirectory\$TextFileName"
[String]$SaveAsPathNew = Join-Path -Path $ScriptDirectory -ChildPath "${MsWordDocTemplateName}-NEW.doc"
#Open Template with MSWord
Try {
$objDoc = $objWord.Documents.Open($WordDocTemplatePath)
} Catch {
[string]$mainErrorMessage = "$($_.Exception.Message) $($_.ScriptStackTrace) $($_.Exception.InnerException)"
Write-Host $mainErrorMessage -ForegroundColor Red
Start-Sleep -Seconds 7
$objDoc.Close()
$objWord.Quit()
}
$objSelection = $objWord.Selection
$objSelection.Find.Forward = 'TRUE'
$objSelection.Find.MatchWholeWord = 'TRUE'
#Replace <package-name>
[String]$FindText = "<package-name>"
[String]$ReplaceWith = "PackageName_v1"
write-host "replacing [$FindText] :" -NoNewline
$objSelection.Find.Execute($FindText,$MatchCase,$MatchWholeWord,$MatchWildcards,$MatchSoundsLike,$MatchAllWordForms,$Forward,$Wrap,$Format,$ReplaceWith,$wdReplaceAll)
#Embed the text file as an object
[System.IO.FileSystemInfo]$TextFileObj = Get-item $TextFilePath
If ( $(Try {Test-Path $($TextFileObj.FullName).trim() } Catch { $false }) ) {
write-host "Embedding [$TextFileName] :" -NoNewline
[String]$FindText = "<TextFile-Placeholder>"
[String]$ReplaceWith = ""
# $objSelection.Find.Execute($FindText,$MatchCase,$MatchWholeWord,$MatchWildcards,$MatchSoundsLike,$MatchAllWordForms,$Forward,$Wrap,$Format,$ReplaceWith,$wdReplaceAll)
#Need code to create a RANGE to the position of <TextFile-Placeholder>
#Embed file into word doc as an object
#$result = $objSelection.InlineShapes.AddOLEObject($null,$TextFileObj.FullName,$false,$true)
$result = $objSelection.Range[0].InlineShapes.AddOLEObject($null,$TextFileObj.FullName,$false,$true) #works too but does the same
Write-host "Success"
} Else {
Write-Host "[$TextFilePath] does not exist!" -ForegroundColor Red
Start-Sleep -Seconds 5
}
Write-Host "Saving updated word doc to [${MsWordDocTemplateName}-NEW.doc] ***"
Start-Sleep -Seconds 2
#List of formats
$AllSaveFormat = [Enum]::GetNames([microsoft.office.interop.word.WdSaveFormat])
$SaveFormat = $AllSaveFormat
$objDoc.SaveAs([ref]$SaveAsPathNew,[ref]$SaveFormat::wdFormatDocument) #Overwrite if exists
$objDoc.Close()
$objWord.Quit()
$null = [System.Runtime.InteropServices.Marshal]::ReleaseComObject([System.__ComObject]$objWord)
[gc]::Collect()
[gc]::WaitForPendingFinalizers()
Remove-Variable objWord
If you have a way to do this in Word 2016 I'll take it too as I'll have to redo it all for office 2016 later on.
UPDATE: Looks like the solution is to create a "Range" where the is located. By default it seems you have one range[0] that represents the whole document.
The closest I came to do this was:
$MyRange = $objSelection.Range[Start:= 0, End:= 7]
But this is not the syntax for PowerShell and it deals only with absolute character positions and not the results of a search for a string.
If I could create a 2nd range maybe this could work:
$objSelection.Range[1].InlineShapes.AddOLEObject($null,$TextFileObj.FullName,$false,$true)
I can't write powershell script for you, but I can describe what you need. You are, indeed, on-track with the Range idea. To get the Range for an entire Word document:
$MyRange = $objDoc.Content
Note that you're free to declare and use as many Range objects as you need. Range is not limited like Selection, of which there can be only one. As long as you don't need the original Range again, go ahead and perform Find on it. If you do need the original Range again, then declare and instantiate a second, instantiating it either as above or, in order to use the original as the starting point:
$MyRange2 = $MyRange.Duplicate
Then use Find on a Range. Note that, when Find is successful the content of the Range is the found term, which puts the "focus" on the correct page. (But will not move the Selection!)
$MyRange.Find.Execute($FindText,$MatchCase,$MatchWholeWord,$MatchWildcards,$MatchSoundsLike,$MatchAllWordForms,$Forward,$Wrap,$Format,$ReplaceWith,$wdReplaceAll)
If you want to test whether Find was successful the Execute method returns a Boolean value (true if Find was successful).
Use something like what follows to insert the file, although I'm not sure OLEObject is the best method for inserting a text file. I'd rather think InsertFile would be more appropriate. An OLEObject requires an OLE Server registered in Windows that supports editing text files; it will be slower and put a field code in the document that will try to update...
$result =$MyRange.InlineShapes.AddOLEObject($null,$TextFileObj.FullName,$false,$true)
Related
Again, thanks for the help with my previous question, but i have hit another problem. As mentioned above the exe in the below code if run in cmd instantly outputs progress as it runs.
However the textbox in the form is blank and there is a delay, where it looks like nothing is happening before the whole output is pasted into the box.
I have looked online and mention of a forms.application do events method but it is not recommended and a bit sloppy.
Any ideas how i could have this live?. I did try a messagebox but i need to close it before the exe would run and i would still have to wait.
I'm referring to the textbox output from xtract-iso.exe in xiso_build function
Code:
Function xiso_build {
Set-Location -Path $PSScriptRoot # change to root folder of this script wherever it's run from
[System.Windows.Forms.Messagebox]::Show("Building, Please Wait...")
$outputBox.text= & .\extract-xiso.exe -r $selected_file 2>&1 | out-string # '2>&1' needs to be there otherwise any errors get outputted to terminal, out-string for better formatting
}
##########################################################################################################################
# the main form
$form = New-Object System.Windows.Forms.Form
$form.StartPosition = 'CenterScreen'
$form.Text = 'Xbox Iso Extractor'
$form.Size = '600,600'
# Choose iso label
# Create a "computer name" label control and add it to the form
# Set label location, text, size, etc
$Label1 = New-Object Windows.Forms.Label
$label1.Font = [System.Drawing.Font]::new("Microsoft Sans Serif", 12, [System.Drawing.FontStyle]::Bold)
$Label1.Size = '180,40'
$Label1.Location = '10,20'
$Label1.Text = "Select An Xbox ISO:"
$Label1.Font.Bold
$form.Controls.Add($Label1)
# textbox
$isotextBox = New-Object System.Windows.Forms.TextBox
$isotextBox.Location = '10,60'
$isotextBox.Size = '320,200'
$form.Controls.Add($isotextBox)
# open file button
$Select_Iso_button = New-Object System.Windows.Forms.button
$Select_Iso_button.Text = 'Choose ISO'
$Select_Iso_button.Size = '100,25'
$Select_Iso_button.Location = '350,60'
$form.controls.Add($Select_Iso_button)
# below code: on click run 'iso_open func above and run global '$selected_file_path' variable from fun, then insert path and file into textbox
# save this selected text into var called $selected_file then execute var
$Select_Iso_button.Add_Click({iso_open; $global:selected_file = $isotextBox.Text = $selected_file_path; $selected_file})
# Output of xtract-iso textbox
$outputBox = New-Object System.Windows.Forms.TextBox #creating the text box
$outputBox.Location = '10,150' #location of the text box (px) in relation to the primary window's edges (length, height)
$outputBox.Size = New-Object System.Drawing.Size(565,200) #the size in px of the text box (length, height)
$outputBox.MultiLine = $True #declaring the text box as multi-line
$outputBox.ScrollBars = "Vertical" #adding scroll bars if required
$form.Controls.Add($outputBox) #activating the text box inside the primary window
# Build Iso Button
$build_button = New-Object System.Windows.Forms.button
$build_button.Text = 'Build ISO'
$build_button.Size = '200,50'
$build_button.Location = '10,360'
# $button.Anchor = 'Bottom,left' # uncomment to move button down to bottom left of app window
$form.Controls.Add($build_button)
$build_button.Add_Click({xiso_build}) # run 'xiso_build' func from above
By default, Out-String collects all input first before outputting the input objects' formatted representations as a single, multiline string.[1]
In order to get near-realtime feedback from the command getting executed, you need to append lines to $output.Text iteratively, as they become available, which you can do via a ForEach-Object call:
$sep = ''; $outputBox.text = ''
& .\extract-xiso.exe -r $selected_file 2>&1 |
ForEach-Object {
# Append the line at hand to the text box.
$outputBox.AppendLine($sep + $_); $sep = "`n"
# Keep the form responsive - THIS MAY NOT BE ENOUGH - see below.
[System.Windows.Forms.Application]::DoEvents()
}
[System.Windows.Forms.Application]::DoEvents()[2] is used to keep the form responsive while the command is executing, but note that this relies on successive output lines generated by your .\extract-xiso.exe call being emitted in relatively short succession (the reason is that the form cannot process any user events while PowerShell is waiting for the next output line to be emitted).
If this doesn't keep your form responsive enough, you'll need a different approach, such as using a background job to run your executable, and using .Show() instead of .ShowDialog() to show your form, with a subsequent, manual [System.Windows.Forms.Application]::DoEvents() loop that periodically polls the background job for new output lines.
The following simplified, self-contained example demonstrates this approach:
Add-Type -AssemblyName System.Windows.Forms
# === Helper functions:
# Launches the external program in a a background job and store
# the job-inf variable in script-level variable $job.
function Start-ExtractionJob {
$script:job = Start-Job {
# Simulate a long-running call to an external program.
# Here is where you'd make your & .\extract-xiso.exe ... call
1..20 | % { cmd /c "echo line $_" 2>&1; Start-Sleep 1 }
}
}
# Adds an output line to the $ouputBox text box
function Add-TextBoxLine {
param([string] $line)
if ($prevLen = $outputBox.Text.Length) {
$sep = "`r`n"
}
else {
$sep = ''
}
$outputBox.AppendText($sep + $line)
# Scroll to the *start* of the new line to prevent horizontal scrolling.
$outputBox.Select($prevLen + $sep.Length, 0)
}
# === Create the form.
$form = [System.Windows.Forms.Form] #{
StartPosition = 'CenterScreen'
Text = 'Xbox Iso Extractor'
Size = '600,600'
}
# Mutiline text box that will receive the program's output, line by line.
$outputBox = [System.Windows.Forms.TextBox] #{
Location = '10,150'
Size = [System.Drawing.Size]::new(565, 200)
MultiLine = $true
ScrollBars = "Vertical" #adding scroll bars if required
}
$form.Controls.Add($outputBox)
# Build Iso Button
$build_button = [System.Windows.Forms.Button] #{
Text = 'Build ISO'
Size = '200,50'
Location = '10,360'
}
$form.Controls.Add($build_button)
# Set up the button event handler for launching
# the external program in a background job.
$build_button.Add_Click({
$this.Enabled = $false
$outputBox.Clear()
Add-TextBoxLine "=== Launching..."
Start-ExtractionJob
})
# --- Manage display of the form and handle job output.
$job = $null
$form.Show() # Show form asynchronously.
while ($form.Visible) {
# While the form is visible, process events periodically.
[System.Windows.Forms.Application]::DoEvents()
# If a job has been launched, collect its output on an ongoing basis.
# If the job has completed, remove it, and reenable the launch button.
if ($job) {
$job | Receive-Job | ForEach-Object {
# $outputBox.Text += $nl + $_; $outputBox.Select($outputBox.Text.Length + $nl.Length, 0); $outputBox.ScrollToCaret()
Add-TextBoxLine $_
}
# Check if the job has terminated, for whatever reason.
if ($job.State -in 'Completed', 'Failed') {
$job | Remove-Job; $job = $null
Add-TextBoxLine '=== Completed'
$build_button.Enabled = $true
}
# Sleep a little.
Start-Sleep -Milliseconds 100
}
}
# ... clean up job, if necessary, ...
[1] While there is a -Stream switch that outputs the lines of the formatted representations one by one, that wouldn't help here, because the key to realtime feedback is to assign to $output.Text iteratively, as the lines become available.
[2] [System.Windows.Forms.Application]::DoEvents() can be problematic in general (it is essentially what the blocking .ShowDialog() call does in a loop behind the scenes), but in this constrained scenario (assuming only one form is to be shown) it should be fine. See this answer for background information.
i'm looking for help in creating a System.Windows.Forms.Textbox that would accept array input and treat it as an array input, rn textbox properties looks something like this:
$boxname.AcceptsReturn = $true
$boxname.MultiLine = $true
$boxname.Autosize = $true
$boxname.AcceptsTab = $false
$boxname.Scrollbars = 'Vertical'
If i enter multiline paths like this:
C:\Windows
C:\Program Files
C:\Temp
$boxname.text is treated like one huge string and won't let my "test-path on each item in array" function work properly, i bound it to this box with .Add_TextChanged, what i'm trying to do is to create a box, that will perform a check that entered paths exist once the box is filled.
Is this even possible without additional parsing ? Maybe i'm missing something.
You need to split the multiline text into separate lines and then loop over these lines to do your checks.
$boxname.Add_TextChanged({
# inside the eventhandler, you can refer to the control object itself using automatic variable $this
$this.Text -split '\r?\n' -ne '' | ForEach-Object {
if (-not(Test-Path -Path $_)) {
# path does not exist, do something..
Write-Host "Path '$_' not found!"
}
}
})
In MS Outlook, is there a way to automatically replace some words like Google, MSN, Facebook, etc (I have an exhausting list in a CSV file), by the hyperlink that redirects to correct website.
So basically when I type google it transforms it to a hyperlink.
My CSV file:
Word, URL
Facebook, https://facebook.com
MSN, https://msn.com
Google, https://google.com
What I have so far is a script that add to the object autocorrect entries a word and replaces it by another word not using a CSV but a word document. But I'm not able to replace it by an hyperlink. It causes an error saying that autocorrect entries accept only string format and not object (hyperlink).
Reference: Add formatted text to Word autocorrect via PowerShell
When I create manually via outlook an hyperlink and I add this hyperlink to autocorrect and I run the following PowerShell script I can't find this autocorrect entry:
(New-Object -ComObject word.application).AutoCorrect.Entries | where{$_.Value -like "*http*"}
I want to adapt this code coming from Use PowerShell to Add Bulk AutoCorrect Entries to Word
If someone has an idea on how to add a hyperlink to the autocorrect entries, I would be grateful.
Thanks!
I finally managed how to add autocorrect entries for both word and outlook.
I need to create a .docx file with 'X row' and '2 Columns', the first column contain the word that i want an autocorrect like 'google' and the second column the 'google' link.
$objWord = New-Object -Com Word.Application
$filename = 'C:\Users\id097109\Downloads\test3.docx'
$objDocument = $objWord.Documents.Open($filename)
$LETable = $objDocument.Tables.Item(1)
$LETableCols = $LETable.Columns.Count
$LETableRows = $LETable.Rows.Count
$entries = $objWord.AutoCorrect.entries
for($r=1; $r -le $LETableRows; $r++) {
$replace = $LETable.Cell($r,1).Range.Text
$replace = $replace.Substring(0,$replace.Length-2)
$withRange = $LETable.Cell($r,2).Range
$withRange.End = $withRange.End -1
# $with = $withRange.Text
Try {
$entries.AddRichText($replace, $withRange) | out-null
}
Catch [system.exception] {
Write-Host $_.Exception.ToString()
}
}
$objDocument.Close()
$objWord.Quit()
[gc]::collect()
[gc]::WaitForPendingFinalizers()
$rc = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($objWord)
This code allow to modify the file Normal.dotm that contains all the autocorrect to an object link (C:\Users{your user id}\AppData\Roaming\Microsoft\Templates)
But then to apply those change to Outlook you have delete the 'NormalEmail.dotm' and the copy/paste 'Normal.dotm' and the rename it to 'NormalEmail.dotm'
This is the script to avoid to do it manually :
$FileName='C:\Users\{your id}\AppData\Roaming\Microsoft\Templates\Normal.dotm'
$SaveTo='C:\Users\{your id}\AppData\Roaming\Microsoft\Templates\NormalEmail.dotm'
Remove-Item –path $SaveTo
$Word = New-Object –ComObject Word.Application
$Document=$Word.Documents.Open($Filename)
$Document.SaveAs($SaveTo)
$Document.Close
I've doing a find and replace in a whole bunch of excel files (to update some links that have moved)
I want to track which files had changes made to them however the result of a replace is always true regardless of if the required text was is there. A find like this:
$worksheet.UsedRange.Find($Text)
will either return null if the text being looked for does not exist or an object if it does, but thats significant extra processing I'd prefer to remove given that I have about 1.2million files to check.
Here is my replace code:
$found=$false
$xlCalculationManual = -4135
$excel = New-Object -comobject Excel.Application
$excel.visible = $false
$excel.AskToUpdateLinks = $false
$excel.DisplayAlerts = $false
$excel.EnableEvents = $false
$workbook=$excel.Workbooks.Open($openfile,0,$false,5,"nopassword","nopassowrd")
$excel.Calculation = $xlCalculationManual
foreach ($worksheet in $workbook.Sheets)
{
foreach ($Text in $FindText)
{
If ($worksheet.UsedRange.replace($Text,$ReplaceText))
{
$found=$true
}
}
}
Looks like Theo is correct on this. I've not found anyway to increase the efficiency by removing the find.
Also it seems that on some files a replace only replaces the first instance and therefore a loop is also required.
Maybe try
$worksheet.UsedRange.replace($Text,$ReplaceText)
If ($?)
Instead of
If ($worksheet.UsedRange.replace($Text,$ReplaceText))
OR you could try searching for the text, and confirm that it found something?
Before your if replace option, try (I apologize, but this code is speculation, and may not full fix it. There is an article on this at https://msdn.microsoft.com/en-us/vba/excel-vba/articles/range-find-method-excel
$confirm = $worksheet.Used.Range.find($text)
if ($confirm -ne $null) {
Echo $true
$worksheet.UsedRange.replace($Text,$ReplaceText)
$confirm = $null
}
else{
echo $false
}
I have a script where I am replacing a bookmark in a word document with text from a xml.
I have hard coded a few words into the word document via the bookmark properties. I am trying to turn the font of the strings I hardcoded into the document into bold and increase the font size
StigData is predefined earlier in my script...
The script works fine its just not bolding the font.
$template = "Template.docx"
$wd = New-Object –comobject Word.Application
$doc=$wd.documents.Add($template)
foreach ($stigDataItem in $stigData)
{
$title = $stigDataItem.title
$check = $stigDataItem.group.Rule.check.'check-content'
$ft = $stigDataItem.group.rule.fixtext.innerxml
$doc.Bookmarks.Item("AllStigInfo").Range.Text ="FixText" + "`r`n" + $ft + "`r`n" +"`r`n"
$doc.Bookmarks.Item("AllStigInfo").Range.Text ="Check Content" + "`r`n" + $check + "`r`n"
$doc.Bookmarks.Item("AllStigInfo").Range.Text =$title + "`r`n"
}
$selection = $wd.selection
$selection.Font.Bold = $True
$stringcheck = "Check Content"
$selection.TypeText($stringcheck)
$doc.SaveAs([ref]$newfile)
$doc.Close()
$wd.Quit()
Remove-Variable wd
I attempted to select the words I need to replace and replace it with a bold font with the section:
$selection = $wd.selection
$selection.Font.Bold = $True
$stringcheck = "Check Content"
$selection.TypeText($stringcheck)
However that just added a bold word to the document.
I am trying to bold everywhere that says FixText, Check Content, and $title. Also making $title a bigger font.
Also tried referring to https://blogs.technet.microsoft.com/heyscriptingguy/2006/02/01/how-can-i-boldface-a-specific-word-throughout-a-microsoft-word-document/ but it didn't work out for me.
I'm not familiar with PowerShell, but the logic seems clear enough so that my suggestion should work:
You should declare a Word.Range object or objects and assign the Bookmark.Range(s) to it/them so that you can afterwards work directly with the Range. (Bookmarks are deleted as soon as something is assigned to their Range, which is why you can't continue on using them for further actions.)
Working with Ranges is preferable to using the Selection object, and you can have more than one (the Application supports only one Selection).
So something like this:
$rngBookmark = $doc.Bookmarks.Item("AllStigInfo").Range
$rngBookmark.Text ="FixText" + "`r`n" + $ft + "`r`n" +"`r`n"
$rngBookmark.Font.Bold = $True
If you want to bold only "FixText" and nothing that follows it in the assignment to the Text property, then:
$rngBookmark = $doc.Bookmarks.Item("AllStigInfo").Range
$rngBookmark.Text ="FixText"
$rngBookmark.Font.Bold = $True
//You need to figure out the syntax for the Collapse method
//I just put in the way I'd use it from VB.NET, with full qualification
$rngBookmark.Collapse(Word.WdCollapseDirection.wdCollapseEnd)
$rngBookmark.Text = "`r`n" + $ft + "`r`n" +"`r`n"
Collapsing the Range is similar to pressing left- / right-arrow on the keyboard to "collapse" a selection to a cursor point.
Discovered two ways of doing this. I used Find and Replace method in Word.
In PowerShell I ended up using
Method 1
$searchText=$replaceText= 'Check Content'
$wdReplaceAll = 2
$wd=New-Object -ComObject Word.Application
$wd.Visible=$false
$doc = $wd.Documents.Open($newfile)
$wd.Selection.Find.Replacement.Font.Bold = $true
$wd.Selection.Find.Execute($searchText, $false, $true, $false, $false, $false, $true, $false, $true, $replaceText, $wdReplaceAll)
Method 2
I also found a VBscript that looks for the specific word and replace and changed it to bold. Using PowerShell to pass the variable $newfile.
Site: https://blogs.technet.microsoft.com/heyscriptingguy/2006/02/01/how-can-i-boldface-a-specific-word-throughout-a-microsoft-word-document/
#invoke script and passed $newfile to vbscript (added in my Powershell script)
$arg1 = "$newfile"
$arg2 = "$title"
$command = “cmd /C cscript 'boldfont.vbs' $arg1”
invoke-expression $command
#vbscript I created
Const wdReplaceAll = 2
Set objWord = CreateObject(“Word.Application”)
objWord.Visible = True
Set objDoc = objWord.Documents.Open(“$newfile”)
Set objSelection = objWord.Selection
objSelection.Find.Text = “Check Content”
objSelection.Find.Forward = TRUE
objSelection.Find.MatchWholeWord = TRUE
objSelection.Find.Replacement.Font.Bold = True
objSelection.Find.Execute ,,,,,,,,,,wdReplaceAll
I will use this to change Fix Text and $title.
I wanted to share it. Might could help someone else.