How I Built My Own PowerShell Multi-File Search Tool
A problem with Windows Search prompted the creation of a custom PowerShell tool for searching code fragments. Learn about the tool's development and its script.
October 22, 2024
It’s funny how one project can spawn another. While deep into a robotics project, I hit a frustrating roadblock: finding a single, elusive code file buried among thousands in an open-source library. The documentation offered little help, and Windows Search wasn’t up to the task. Out of this difficulty, I realized I needed a better solution, so I decided to build one: a PowerShell-based tool for searching code.
Windows Search usually does a great job searching through multiple files, especially when indexed, but it was designed to find text. It struggles with the special characters often used in code. In contrast, PowerShell can search through code, but writing a script for this purpose was far more complex than I had imagined. By the time I finished, I had created a full-blown search tool that made locating files and code fragments much easier—and along the way, I learned quite a bit about PowerShell.
In this article, I will walk through the process of developing the tool, share lessons learned, and break down two significant challenges I faced. At the end, I will provide the complete source code.
The Problem With Special Characters
One of the first hurdles I encountered was handling special characters in code. Like Windows Search, PowerShell can struggle with special characters when searching for code. I needed to convert the input string into a “safe” format before taking action on it, and I did this by using regular expressions (regex). In the code excerpt below, I created a variable called $SafeInput that contains a regular expression of the user input.
# Prompt the user for input
$UserInput = Read-Host "Please enter a string to search for"
# Search the C:\Temp folder and all subfolders for files containing the input string
$SearchPath = "C:\Temp"
# Escape any special characters in the user input to avoid issues
$SafeInput = [Regex]::Escape($UserInput)
# Perform the search
$Files = Get-ChildItem -Path $SearchPath -Recurse -File |
Select-String -Pattern $SafeInput
In Figure 1, you can see that I used the input Write-Host “Hello World!” and converted it into a regular expression. PowerShell automatically placed a backslash before each space within the input string. While a space is unlikely to cause issues in the context of a search string, this example demonstrates how you can use regular expressions to handle potentially problematic characters.
Figure 1. I am converting the user input into a regular expression.
Searching Files With PowerShell
Having seen how the script handles user input, let’s discuss what the script does. The script allows users to enter a string of characters they want to search for. Upon doing so, PowerShell will look through all files in a designated location and its subfolders for a match. The search location is currently hardcoded to C:\Temp, but I plan to add an option where users can specify their desired search location.
Once the search is complete, PowerShell populates the bottom-left portion of the GUI with a list of files that contain the search term. When you click on a file, its contents appear in the lower-right portion of the GUI. PowerShell also highlights each time the search term appears in the file.
In Figure 2, you can see that when I searched for the word “Button,” the tool returned a list of PowerShell scripts within the search location that contained the word. Clicking on a script, like Calc.ps1, displays the script’s contents with each instance of “Button” highlighted. Of course, I didn’t have to search for a word—I could have just as easily searched for special characters.
Figure 2. My multi-file search tool has identified all the files containing my search term.
Selecting a File
At the start of the article, I mentioned two challenges I encountered while developing my PowerShell-based multi-file search tool. The most daunting of these was implementing a feature that allowed users to click on a file and view its contents.
If my goal had been to display a list of files that met the search criteria, I could have easily stored the search results in an array and shown the array’s contents in a text box. However, text boxes, to my knowledge, do not support clickable elements. Users can’t click on items or engage with the results directly.
I ended up solving this problem by dynamically creating a list box. A list box functions like a dropdown menu, allowing me to add each search result as a menu choice in real time as my script processes the files. This way, users could interact directly with the results rather than view a static list.
While the list box solution worked, it raised an intriguing question. If you look back at Figure 2, the search results shown in the left side of the window seem like they are in a text box instead of a list box. When I first had the idea to build a multi-file search tool, the interface illustrated in Figure 2 was what I had in mind. I wanted the search results to be immediately visible within a windowpane so users wouldn’t have to pick a file from a dropdown menu.
So, how did I accomplish this? Well, I had to use a bit of trickery.
Although the search results seem to be in a standard windowpane, they are in a list box. I hid the elements that would make it look like a list box. Let me show you how I did this.
$Files = Get-ChildItem -Path $SearchPath -Recurse | Where-Object { -not $_.PSIsContainer } | Select-Object -ExpandProperty FullName
ForEach ($File in $Files){
$FileContent = Get-Content -Path $File -Raw
If (Select-String -Path $File -Pattern $SafeInput)
{
[void] $FileListBox.Items.Add($File)
}
}
})
In the code excerpt above, I create the list box called $FileListBox. This section performs the search and adds each search result to the list box, one by one. The result is a dropdown menu that contains all files that match the search criteria.
Here is the code that makes the list box look more like a text box:
# Create the left panel
$VisibleItems = 25
$ListBoxHeight = $VisibleItems * 28
$FileListBox = New-Object System.Windows.Forms.ListBox
$FileListBox.Location = New-Object System.Drawing.Point(100,300)
$FileListBox.Font = New-Object System.Drawing.Font("Arial", 16)
$FileListBox.Size = New-Object System.Drawing.Size (450, $ListBoxHeight)
You will notice that I started this section by defining two variables: $VisibleItems and $ListBoxHeight. When you create a list box, the list box is collapsed by default. However, you can have Windows automatically expose any number of list box items. Often, people show the first item within the list and treat it as a default selection. In this case, I chose to display 25 items.
Why 25? It is not an arbitrary number. I selected it based on the dimensions of the window and font size. A maximum of 25 items fit within the window.
Unfortunately, PowerShell doesn’t let you directly specify how many items to show. Instead, you must set the list box size in pixels. That’s where the $ListBoxHeight variable comes into play. Each row of text, with my chosen font size, takes up 28 vertical pixels (including spacing). So, I multiplied 28 pixels by the number of rows I wanted to display. The last line of the code excerpt tells PowerShell that the list box should be 450 pixels wide, and its height should correspond to the $ListBoxHeight variable.
But what happens when the search returns more than 25 results? In that case, a scroll bar appears automatically within the interface, allowing users to scroll through the results.
Output Formatting
The other challenge I ran into was displaying file contents correctly. Thankfully, this problem was easy to fix.
When I first created the script, PowerShell ignored all the line breaks in the files returned by the search. Figure 3 shows you what the file contents looked like. You can see the difference clearly when comparing Figure 2 to Figure 3.
Figure 3. PowerShell initially ignored line breaks within the files.
The code excerpt below is responsible for displaying the file contents when a user clicks on a file:
# Add an event handler to respond to item selection
$FileListbox.Add_SelectedIndexChanged({
$SelectedItem = $FileListbox.SelectedItem
$FileContents = Get-Content -Path $SelectedItem -RAW
$FileContentsTextBox.Text = $FileContents
This code block detects whether the user has clicked anything within the list box. I didn’t want to require the user to click a submit button after making a selection, so the script checks if a click has occurred.
The clicked item gets written to a variable called $SelectedItem, which contains the full file path and name associated with the file that needs to be displayed.
From there, I create another variable, $FileContents, to retrieve the actual contents of the selected file. Finally, the last line of code displays the file contents in the text box for the user to read.
So, here is how I solved the problem of PowerShell ignoring line breaks: Appending -RAW to the second-to-last line of code in the excerpt. Doing so causes PowerShell to preserve the file’s original formatting, including the line breaks.
While the code block above dealt with the line break issue, it didn’t handle the next piece of the puzzle: highlighting the search term within the file. For that, I used this next block of code:
$WordToHighlight = $SafeInput
$Index = 0
# Loop to find and highlight all instances of the word
while (($Index = $FileContentsTextBox.Find($WordToHighlight, $Index, [System.Windows.Forms.RichTextBoxFinds]::None)) -ge 0) {
# Select the word
$FileContentsTextBox.Select($Index, $WordToHighlight.Length)
# Apply the Highlight (yellow background)
$FileContentsTextBox.SelectionBackColor = [System.Drawing.Color]::Yellow
# Move the StartIndex forward for the next search
$Index += $WordToHighlight.Length
}
})
The Full Source Code
Now that I have covered some of the more interesting aspects of the script, I would like to share the source code with you. Below is my script in its entirety:
Add-Type -AssemblyName System.Windows.Forms
# Create a form
$Form = New-Object System.Windows.Forms.Form
$Form.Text = "Multi-File Search Tool"
$Form.Width = 1920
$Form.Height = 1080
# $InputLabel
# Create the label to display instructions
$InputLabel = New-Object Windows.Forms.Label
$InputLabel.Text = "Enter the text that you want to search for and click Submit."
$InputLabel.Font = New-Object Drawing.Font("Arial", 24, [System.Drawing.FontStyle]::Bold)
$InputLabel.AutoSize = $true
$InputLabel.Location = New-Object Drawing.Point(100, 50)
$InputLabel.ForeColor = [System.Drawing.Color]::Black
# Create the label to display instructions
$ResultsLabel = New-Object Windows.Forms.Label
$ResultsLabel.Text = "Files Containing Search String"
$ResultsLabel.Font = New-Object Drawing.Font("Arial", 16, [System.Drawing.FontStyle]::Bold)
$ResultsLabel.AutoSize = $true
$ResultsLabel.Location = New-Object Drawing.Point(120, 270)
$ResultsLabel.ForeColor = [System.Drawing.Color]::Black
# Create the label to display instructions
$FileContentsLabel = New-Object Windows.Forms.Label
$FileContentsLabel.Text = "Selected File's Contents"
$FileContentsLabel.Font = New-Object Drawing.Font("Arial", 16, [System.Drawing.FontStyle]::Bold)
$FileContentsLabel.AutoSize = $true
$FileContentsLabel.Location = New-Object Drawing.Point(600, 270)
$FileContentsLabel.ForeColor = [System.Drawing.Color]::Black
# $InputBox
$InputBox = New-Object System.Windows.Forms.textbox
$InputBox.Text = "This is where the query goes"
$InputBox.Multiline = $True
$InputBox.Size = New-Object System.Drawing.Size(500,50)
$InputBox.Location = new-object System.Drawing.Size(100,100)
$InputBox.Font=New-Object System.Drawing.Font("Arial", 16)
# $SubmitButton
$SubmitButton = New-Object System.Windows.Forms.Button
$SubmitButton.Location = New-Object System.Drawing.Size (150,170)
$SubmitButton.Size = New-Object System.Drawing.Size(100,50)
$SubmitButton.Font=New-Object System.Drawing.Font("Arial", 16)
$SubmitButton.BackColor = "LightGray"
$SubmitButton.Text = "Submit"
$SubmitButton.Add_Click({
$UserInput = $InputBox.Text
$SearchPath = 'C:\Temp'
#Clear any results from previous searches
[Void] $FileListBox.Items.Clear()
# Escape any special characters in the user input to avoid issues
$Global:SafeInput = [Regex]::Escape($userInput)
# Get a list of all files and their path
$Files = Get-ChildItem -Path $SearchPath -Recurse | Where-Object { -not $_.PSIsContainer } | Select-Object -ExpandProperty FullName
ForEach ($File in $Files){
$FileContent = Get-Content -Path $File -Raw
If (Select-String -Path $File -Pattern $SafeInput)
{
[void] $FileListBox.Items.Add($File)
}
}
})
# $ExitButton
$ExitButton = New-Object System.Windows.Forms.Button
$ExitButton.Location = New-Object System.Drawing.Size (300,170)
$ExitButton.Size = New-Object System.Drawing.Size(100,50)
$ExitButton.Font=New-Object System.Drawing.Font("Arial", 16)
$ExitButton.BackColor = "LightGray"
$ExitButton.Text = "Exit"
$ExitButton.Add_Click({
$Form.Close()
})
# Create the left panel
$VisibleItems = 25
$ListBoxHeight = $VisibleItems * 28
$FileListBox = New-Object System.Windows.Forms.ListBox
$FileListBox.Location = New-Object System.Drawing.Point(100,300)
$FileListBox.Font = New-Object System.Drawing.Font("Arial", 16)
$FileListBox.Size = New-Object System.Drawing.Size (450, $ListBoxHeight)
# Add an event handler to respond to item selection
$FileListbox.Add_SelectedIndexChanged({
$SelectedItem = $FileListbox.SelectedItem
$FileContents = Get-Content -Path $SelectedItem -RAW
$FileContentsTextBox.Text = $FileContents
$WordToHighlight = $SafeInput
$Index = 0
# Loop to find and highlight all instances of the word
while (($Index = $FileContentsTextBox.Find($WordToHighlight, $Index, [System.Windows.Forms.RichTextBoxFinds]::None)) -ge 0) {
# Select the word
$FileContentsTextBox.Select($Index, $WordToHighlight.Length)
# Apply the Highlight (yellow background)
$FileContentsTextBox.SelectionBackColor = [System.Drawing.Color]::Yellow
# Move the StartIndex forward for the next search
$Index += $WordToHighlight.Length
}
})
# Create the right panel
$FileContentsTextBox = New-Object System.Windows.Forms.RichTextBox
$FileContentsTextBox.Text = ""
$FileContentsTextBox.Multiline = $True
$FileContentsTextBox.Size = New-Object System.Drawing.Size(1200,700)
$FileContentsTextBox.Location = new-object System.Drawing.Size(600,300)
$FileContentsTextBox.Font=New-Object System.Drawing.Font("Arial", 16)
$FileContentsTextBox.Scrollbars = [System.Windows.Forms.ScrollBars]::Both
# Add panels to the form
$Form.Controls.Add($InputLabel)
$Form.Controls.Add($ResultsLabel)
$Form.Controls.Add($FileContentsLabel)
$Form.Controls.Add($InputBox)
$Form.Controls.Add($SubmitButton)
$Form.Controls.Add($ExitButton)
$Form.Controls.Add($FileListBox)
$Form.Controls.Add($FileContentsTextBox)
# Show the form
$Form.Add_Shown({$Form.Activate()})
[void] $Form.ShowDialog()
Next Steps
As useful as the multi-file search tool is, there are several ways to improve it.
For example, I have considered building a text editor directly into the tool, allowing users to modify files without switching programs. I created a standalone text editor using PowerShell that I will likely cover in a future article. I am using the PowerShell-based editor as a proof-of-concept project, but I plan to integrate parts of it into the multi-file search tool.
Future updates could include adding the line numbers to files and perhaps even implementing syntax highlighting.
About the Author
You May Also Like