How to search Windows Search Indexed in file - powershell

I have files indexed by the Windows Search service. I need function, which can find some string in text files. I have script in PowerShell, but it didn't work fine.
function search {
param($path, $word)
$c = $path + "\%"
$query = "SELECT
System.ItemName, System.ItemPathDisplay
FROM SystemIndex
WHERE System.ItemPathDisplay LIKE '$c' AND CONTAINS('$word')"
$ADOCommand = New-Object -ComObject ADODB.Command
$ADOConnection = New-Object -ComObject ADODB.Connection
$RecordSet = New-Object -ComObject ADODB.RecordSet
$ADOConnection.Open("Provider=Search.CollatorDSO;Extended Properties='Application=Windows';")
$RecordSet.Open($query, $ADOConnection)
try { $RecordSet.MoveFirst() }
catch [System.Exception] { "no records returned" }
while (-not($RecordSet.EOF)) {
if ($locatedFile) { Remove-Variable locatedFile }
$locatedFile = New-Object -TypeName PSObject
Add-Member -InputObject $locatedFile -MemberType NoteProperty -Name 'Name' -Value ($RecordSet.Fields.Item("System.ItemName")).Value
Add-Member -InputObject $locatedFile -MemberType NoteProperty -Name 'Path' -Value ($RecordSet.Fields.Item("System.ItemPathDisplay")).Value
$locatedFile
$RecordSet.MoveNext()
}
$RecordSet.Close()
$ADOConnection.Close()
$RecordSet = $null
$ADOConnection = $null
[gc]::Collect()
}
If $word = "Hello" it works fine for files where we have
*some text * Hello * some text*
in the file, but not when we have Hello without spaces like:
HelloWorld
We can't also search when $word is a phrase,for example "Hello World".
Anyone know how to fix it?

I believe the issue is with the CONTAINS in your query. You should add asterisk (*) wildcard character to the word that you search.
So instead of:
WHERE System.ItemPathDisplay LIKE '$c' AND CONTAINS('$word')
please try:
WHERE System.ItemPathDisplay LIKE '$c' AND CONTAINS('*$word*')

Related

Replacing multiple strings in a word doc in PowerShell

I'm trying to replace multiple strings in a word document using PowerShell, but only one string is replaced when running the code below:
#Includes
Add-Type -AssemblyName System.Windows.Forms
#Functions
#Function to find and replace in a word document
function FindAndReplace($objSelection, $findText,$replaceWith){
$matchCase = $true
$matchWholeWord = $true
$matchWildcards = $false
$matchSoundsLike = $false
$matchAllWordForms = $false
$forward = $true
$wrap = [Microsoft.Office.Interop.Word.WdFindWrap]::wdReplaceAll
$format = $false
$replace = [Microsoft.Office.Interop.Word.WdFindWrap]::wdFindContinue
$objSelection.Find.Execute($findText,$matchCase,$matchWholeWord,$matchWildcards,$matchSoundsLike,$matchAllWordForms,$forward,$wrap,$format,$replaceWith, $replace) > $null
}
$item1 = "Should"
$item2 = "this"
$item3 = "work"
$item4 = "?"
$fileName = "NewFile"
#Opens a file browsers to select a word document
$FileBrowser = New-Object System.Windows.Forms.OpenFileDialog -Property #{
InitialDirectory = [Environment]::GetFolderPath('Desktop')
Filter = 'Documents (*.docx)|*.docx'
}
Write-Host "Select word template file"
$FileBrowser.ShowDialog()
$templateFile = $FileBrowser.FileName
$word = New-Object -comobject Word.Application
$word.Visible = $false
$template = $word.Documents.Open($templateFile)
$selection = $template.ActiveWindow.Selection
FindAndReplace $selection '#ITEM1#' $item1
FindAndReplace $selection '#ITEM2#' $item2
FindAndReplace $selection '#ITEM3#' $item3
FindAndReplace $selection '#ITEM4#' $item4
$fileName = $fileName
$template.SaveAs($fileName)
$word.Quit()
If I comment out FindAndReplace the first one that runs works, but subsequent calls do not.
For example running this as is results in:
Input Output
#ITEM1# Should
#ITEM2# #ITEM2#
#ITEM3# #ITEM3#
#ITEM4# #ITEM4#
I'm not sure what I'm missing, any help would be appreciated
As was suggested it appears that the cursor was not returning to the beginning of the document. I added the following code:
Set-Variable -Name wdGoToLine -Value 3 -Option Constant
Set-Variable -Name wdGoToAbsolute -Value 1 -Option Constant
To the beginning of my script and:
$objSelection.GoTo($wdGoToLine, $wdGoToAbsolute, 1) > $null
as the first line in my FindAndReplace function, and now it works as expected.
There may be a more elegant solution, but this works for me

How to access contents of OneNote Page?

I'm new to powershell, and have gotten this far looking for code examples online, however I'm unable to find any examples that show me how to get at the actual page contents of a OneNote page with Powershell.
$OneNote = New-Object -ComObject OneNote.Application
[xml]$Hierarchy = ""
$OneNote.GetHierarchy("",
[Microsoft.Office.InterOp.OneNote.HierarchyScope]::hsPages, [ref]$Hierarchy)
foreach ($notebook in $Hierarchy.Notebooks.Notebook ) {
$notebook.Name
"=============="
foreach ($section in $notebook.Section) {
"# TAB: " + $section.Name
foreach ($page in $section.page) {
" " + $page.Name
#$page.GetAttribute.ToString()
#$page.Attributes
#$page.InnerText
# How do I get to the contents of the page?
}
}
" "
}
Since I was looking for a solution today on how to read OneNote notebook contents using powershell, I was initially pleased that I found this article here. But then I didn't get any further at first because I kept getting an error; namely in the line
Select-Xml -xml($xml.Value) -Namespace $schema -Xpath "//one:Notebook/one:Section" |foreach{
It always said that $xml.Value has the value NULL, and it cannot be cast to the type XmlNode[].
After some trial and error, I found that all I had to do was simply change this line to
Select-Xml -xml $xml -Namespace $schema -Xpath "//one:Notebook/one:Section" |foreach{
so just omit the .Value and the surrounding brackets.
And yes, of course: Thanks to the article creator! I would never have come up with this solution on my own.
You are almost there. Seeing that OneNote content is within an XMLElement. you will need to use Select-Xml to get the information.
Below is an example of how to get the Page property of the XMLElement:
$OneNote = New-Object -ComObject OneNote.Application
$schema = #{one=”http://schemas.microsoft.com/office/onenote/2013/onenote”}
[xml]$Hierarchy = ""
$OneNote.GetHierarchy("",
[Microsoft.Office.InterOp.OneNote.HierarchyScope]::hsPages, [ref]$Hierarchy)
foreach ($notebook in $Hierarchy.Notebooks.Notebook ) {
$notebook.Name
"=============="
foreach ($section in $notebook.Section) {
foreach ($page in $section.page) {
" " + $page
foreach($xml in $page)
{
Select-Xml -xml($xml.Value) -Namespace $schema -Xpath "//one:Notebook/one:Section" |foreach{
$node = $psitem.node
$npath = Split-Path -Path $node.Path -Parent
#This is where all the magic happens
$props = [ordered]#{
Workbook= Split-Path -Path $npath -Leaf
Section = $node.name
Child = $node.ChildNodes
Page = $node.Page #This is your page content
}
New-Object -TypeName PSObject -Property $props
}
}
}
}
}

How do I use PowerShell to pull headers from a OneNote document

Background:
In my work environment, we have a transitional location for our knowledgebase notes. These reside in a number of OneNote 2016 workbooks which have been maintained over years. I am currently in the middle of delegating content update efforts to our staff and part of this work involves importing all our OneNote notebook names and section names into an excel spreadsheet for hierarchy management.
Task: I spent ages looking online for an easy and quick way to export hierarchy information from OneNote to csv using PowerShell and could not for the life of me find an easy way that worked. The following code resonated through the interwebs but each time I tried to run the code, I kept getting errors.
$onenote = New-Object -ComObject OneNote.Application
$scope = [Microsoft.Office.Interop.OneNote.HierarchyScope]::hsPages
[ref]$xml = $null
$onenote.GetHierarchy($null, $scope, $xml)
$schema = #{one=”http://schemas.microsoft.com/office/onenote/2013/onenote”}
$xpath = “//one:Notebook/one:Section”
Select-Xml -Xml (
$xml.Value) -Namespace $schema -XPath $xpath |
foreach {
$node = $psitem.Node
$npath = Split-Path -Path $node.Path -Parent
$props = [ordered]#{
Workbook = Split-Path -Path $npath -Leaf
Section = $node.Name
}
New-Object -TypeName PSObject -Property $props
}
Error:
The error I would get from executing this code was as follows:
value of type "System.String" to type "System.Xml.XmlNode".
At line:10 char:17
+ Select-Xml -Xml (
Solution:
In the end I had to break down the established connection to the Onenote Application and found a workable solution for OneNote 2016. I've provided my solution but am keen to hear of any other possible ways to manipulate this data effectively in the future:
Function Get-OneNoteHeaders{
[CmdletBinding()]
Param()
Begin
{
$onenote = New-Object -ComObject OneNote.Application
$scope = [Microsoft.Office.Interop.OneNote.HierarchyScope]::hsPages
[ref]$xml = $null
$csvOutput = "c:\temp\onenote-headers.csv"
}
Process
{
$onenote.GetHierarchy($null, $scope, $xml)
[xml]$result = ($xml.Value)
Foreach($notebook in $($result.DocumentElement.Notebook)){
Add-content -Path $csvOutput -Value "$($notebook.name)"
Foreach($section in $($notebook.section)){
Add-content -Path $csvOutput -Value ",$($section.name)"
Foreach($page in $section.page){
Add-content -Path $csvOutput -Value ",,$($page.name)"
}
}
}
}
End{}
}
#Get-OneNoteHeaders

Is there a PowerShell equivalent tracert that works in version 2?

I'm using PSVersion 2.0 and I was wondering is there a equivalent to the traceroute for it?
I'm aware that on PowerShell v4 there is Test-NetConnection cmdlet to do tracert but v2?! It can be done like:
Test-NetConnection "IPaddress/HOSTaname" -TraceRoute
Thanks
As mentioned in the comment, you can make your own "poor-mans-PowerShell-tracert" by parsing the output from tracert.exe:
function Invoke-Tracert {
param([string]$RemoteHost)
tracert $RemoteHost |ForEach-Object{
if($_.Trim() -match "Tracing route to .*") {
Write-Host $_ -ForegroundColor Green
} elseif ($_.Trim() -match "^\d{1,2}\s+") {
$n,$a1,$a2,$a3,$target,$null = $_.Trim()-split"\s{2,}"
$Properties = #{
Hop = $n;
First = $a1;
Second = $a2;
Third = $a3;
Node = $target
}
New-Object psobject -Property $Properties
}
}
}
By default, powershell formats objects with 5 or more properties in a list, but you can get a tracert-like output with Format-Table:
Fixed a few bugs in " Mid-Waged-Mans-Tracert" Version, modularized it, and added some customization pieces. #MrPaulch had a great PoC.
function Invoke-Traceroute{
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true,Position=1)]
[string]$Destination,
[Parameter(Mandatory=$false)]
[int]$MaxTTL=16,
[Parameter(Mandatory=$false)]
[bool]$Fragmentation=$false,
[Parameter(Mandatory=$false)]
[bool]$VerboseOutput=$true,
[Parameter(Mandatory=$false)]
[int]$Timeout=5000
)
$ping = new-object System.Net.NetworkInformation.Ping
$success = [System.Net.NetworkInformation.IPStatus]::Success
$results = #()
if($VerboseOutput){Write-Host "Tracing to $Destination"}
for ($i=1; $i -le $MaxTTL; $i++) {
$popt = new-object System.Net.NetworkInformation.PingOptions($i, $Fragmentation)
$reply = $ping.Send($Destination, $Timeout, [System.Text.Encoding]::Default.GetBytes("MESSAGE"), $popt)
$addr = $reply.Address
try{$dns = [System.Net.Dns]::GetHostByAddress($addr)}
catch{$dns = "-"}
$name = $dns.HostName
$obj = New-Object -TypeName PSObject
$obj | Add-Member -MemberType NoteProperty -Name hop -Value $i
$obj | Add-Member -MemberType NoteProperty -Name address -Value $addr
$obj | Add-Member -MemberType NoteProperty -Name dns_name -Value $name
$obj | Add-Member -MemberType NoteProperty -Name latency -Value $reply.RoundTripTime
if($VerboseOutput){Write-Host "Hop: $i`t= $addr`t($name)"}
$results += $obj
if($reply.Status -eq $success){break}
}
Return $results
}
I must admit I wanted to see whether someone already did this.
You can use the .Net Framework to implement a not-so-poor-mans-traceroute as a Powershell Script
Here a primer, that works fast, but dangerous.
Also, no statistics.
#
# Mid-Waged-Mans-Tracert
#
$ping = new-object System.Net.NetworkInformation.Ping
$timeout = 5000
$maxttl = 64
$address = [string]$args
$message = [System.Text.Encoding]::Default.GetBytes("MESSAGE")
$dontfragment = false
$success = [System.Net.NetworkInformation.IPStatus]::Success
echo "Tracing $address"
for ($ttl=1;$i -le $maxttl; $ttl++) {
$popt = new-object System.Net.NetworkInformation.PingOptions($ttl, $dontfragment)
$reply = $ping.Send($address, $timeout, $message, $popt)
$addr = $reply.Address
$rtt = $reply.RoundtripTime
try {
$dns = [System.Net.Dns]::GetHostByAddress($addr)
} catch {
$dns = "-"
}
$name = $dns.HostName
echo "Hop: $ttl`t= $addr`t($name)"
if($reply.Status -eq $success) {break}
}
Edit:
Removed some of the danger by adding a catch statement.
The only danger that is still present is the fact that we only send a single request per hop, which could mean that we don't reach a hop due to a innocent package drop.
Resolving that issue remains a readers exercise.
Hint: (Think of loops within loops)
Bonus: We now attempt to get the dns entry of each hop!
With at least PS 5 you can
Test-Netconnection stackoverflow.com -TraceRoute

Populating a datagridview

I ma working on a form that will search all connected drives for PST files.
I can get it working with the following command:-
Get-PSDrive -PSProvider "filesystem"|%{get-childitem $_.root -include *.pst -r}|select name, directoryname, #{name="Size (GB)";expression ={"{0:N2}" -f ($_.length/1GB)}}
The only problem is it takes about 45 minutes to run through all the drives and finish the search. I was thinking of trying to speed it up by using the windows search index.
I have got this....
function Searchindex{
$query="SELECT System.ItemName, system.ItemPathDisplay, System.ItemTypeText, System.Size FROM SystemIndex where system.itemtypetext = 'outlook data file'"
$objConnection = New-Object -ComObject adodb.connection
$objrecordset = New-Object -ComObject adodb.recordset
$objconnection.open("Provider=Search.CollatorDSO;Extended Properties='Application=Windows';")
$objrecordset.open($query, $objConnection)
$array=#()
Try { $objrecordset.MoveFirst() }
Catch [system.exception] { "no records returned" }
do
{
Write-host ($objrecordset.Fields.Item("System.ItemName")).value `
($objrecordset.Fields.Item("System.ItemPathDisplay")).value `
($objrecordset.Fields.Item("System.ITemTypeText")).value `
($objrecordset.Fields.Item("System.Size")).value
if(-not($objrecordset.EOF)) {$objrecordset.MoveNext()}
} Until ($objrecordset.EOF)
$objrecordset.Close()
$objConnection.Close()
$objrecordset = $null
$objConnection = $null
[gc]::collect()
}
this outputs the details to the screen in a few seconds which is perfect but I can't work out how to display it in a datagrid view.
I am using primal form to create the forms.
Once the data is populated in the datagridview I want to be able to select records and copy them to a new location
Can anyone help?
TIA
Andy
I'm not familiar with DataGridView but I feel if you had an object you would be better capable to manipulate it.
function Searchindex{
$query="SELECT System.ItemName, system.ItemPathDisplay, System.ItemTypeText, System.Size FROM SystemIndex where system.itemtypetext = 'outlook data file'"
$objConnection = New-Object -ComObject adodb.connection
$objrecordset = New-Object -ComObject adodb.recordset
$objconnection.open("Provider=Search.CollatorDSO;Extended Properties='Application=Windows';")
$objrecordset.open($query, $objConnection)
$array=#()
Try { $objrecordset.MoveFirst() }
Catch [system.exception] { "no records returned" }
do
{
$array += [pscustomobject]#{
Name = ($objrecordset.Fields.Item("System.ItemName")).value
Path = ($objrecordset.Fields.Item("System.ItemPathDisplay")).value
TypeText = ($objrecordset.Fields.Item("System.ITemTypeText")).value
Size = ($objrecordset.Fields.Item("System.Size")).value
}
If(-not($objrecordset.EOF)) {$objrecordset.MoveNext()}
} Until ($objrecordset.EOF)
$objrecordset.Close()
$objConnection.Close()
$objrecordset = $null
$objConnection = $null
[gc]::collect()
$array
}
This will send out a custom PowerShell object array. You already had the variable $array initialized. We just needed to populate it.
Then you could use something like this to filter out the files you are looking for.
Searchindex | Out-GridView -PassThru
After hitting Ok it will only output the records selected.
DataGridView
with multiselect and return
$global:results = #()
#...searchindex function is here ....
$form = New-Object System.Windows.Forms.Form
$form.Size = New-Object System.Drawing.Size(900,600)
$dataGridView = New-Object System.Windows.Forms.DataGridView
$dataGridView.Size=New-Object System.Drawing.Size(800,400)
$dataGridView.SelectionMode = 'FullRowSelect'
$dataGridView.MultiSelect = $true
$go = New-Object System.Windows.Forms.Button
$go.Location = New-Object System.Drawing.Size(300,450)
$go.Size = New-Object System.Drawing.Size(75,23)
$go.text = "Select"
$form.Controls.Add($go)
$form.Controls.Add($dataGridView)
$arraylist = New-Object System.Collections.ArrayList
$arraylist.AddRange((Searchindex))
$dataGridView.DataSource = $arraylist
$dataGridView.Columns[0].width = 240
$go.Add_Click(
{
$dataGridView.SelectedRows| ForEach-Object{
$global:results += [pscustomobject]#{
Name = $dataGridView.Rows[$_.Index].Cells[0].Value
Path = $dataGridView.Rows[$_.Index].Cells[1].Value
TypeText = $dataGridView.Rows[$_.Index].Cells[2].Value
Size = $dataGridView.Rows[$_.Index].Cells[3].Value
}
$form.Close()
}
})
$form.ShowDialog()
$global:results
There is a lot to cover here but look at the examples and let me know how this works for you. It will return all selected rows back as objects in the global variable $global:results. It needs to be global as output does not persist outside the $go.Add_Click. The searchindex function is there but omitted in the second code sample to save space.