Skip to content

Instantly share code, notes, and snippets.

@tsmarvin
Last active April 8, 2025 00:21
Show Gist options
  • Save tsmarvin/3e9c1a092214fd8269cbe4c2170d49a6 to your computer and use it in GitHub Desktop.
Save tsmarvin/3e9c1a092214fd8269cbe4c2170d49a6 to your computer and use it in GitHub Desktop.
TM-WindowsUtility

TM-WindowsUtility Module

Introduction

TM-WindowsUtility is a PowerShell module that provides a set of utilities for managing and interacting with Windows features. It encompasses a variety of functions that extend the built-in Windows management capabilities provided directly from PowerShell.

This module is part of a suite of tools designed to improve and streamline the PowerShell commandline and scripting experience.
Check out the rest of the modules by visiting my page on the PowerShell Gallery.

Features

  • Get-COMDetails: Retrieves extended file properties using COM (Component Object Model) and returns the key/value pairs as a PSCustomObject.
  • Get-DNSNamesBySubnet: Queries DNS names for devices in a specified subnet, returning their IP addresses, device names, and ping statuses.
  • New-ConsoleArgWriter: Generates an executable that writes input arguments to the console, useful for testing and debugging scripts that utilize command-line applications.
  • Search-Registry: Allows searching the Windows Registry for keys, value names, or value data matching specified criteria, outputting the results as PSCustomObjects.
  • Set-WindowFocus: Shifts focus to a specified window by invoking Windows APIs to manage window visibility and focus.
  • Wait-Active: Keeps the computer active for a specified amount of time by simulating user activity.
  • touch: Mimics the Unix touch command to create a new file at the specified path if it does not already exist.
  • tail: Emulates the Unix tail command to display the last 'n' lines of a file, with the option to follow the file as it grows.

Requirements

  • Windows PowerShell 5.1+, or PowerShell Core 7+.
  • Some functions may require administrative privileges to execute properly.
  • Required modules: TM-ValidationUtility, and TM-RandomUtility.

Installation

Install TM-WindowsUtility from the PowerShell Gallery:

Install-Module TM-WindowsUtility -Scope CurrentUser -Repository PSGallery

For manual installation, download the module files and place them in a "TM-WindowsUtility" folder in your PowerShell modules directory ($Env:PSModulePath).

MIT License
Copyright (c) 2023 Taylor Marvin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@{
# Script module or binary module file associated with this manifest.
RootModule = 'TM-WindowsUtility.psm1'
# Version number of this module.
ModuleVersion = '0.0.10'
# Supported PSEditions
CompatiblePSEditions = @('Desktop','Core')
# ID used to uniquely identify this module
GUID = '0b5be38d-5921-4346-941e-3d6a8352e1ab'
# Author of this module
Author = 'Taylor Marvin'
# Company or vendor of this module
CompanyName = 'N/A'
# Copyright statement for this module
Copyright = 'Taylor Marvin (2023)'
# Description of the functionality provided by this module
Description = 'Provides a set of utility functions that run on windows.'
# Minimum version of the PowerShell engine required by this module
PowerShellVersion = '5.1'
# Modules that must be imported into the global environment prior to importing this module
RequiredModules = @(
@{ModuleName='TM-RandomUtility'; ModuleVersion='0.0.10'; GUID='c07a9da5-9562-42b6-8aba-1279fdb25a8e'},
@{ModuleName='TM-ValidationUtility'; ModuleVersion='0.0.4'; GUID='1f1eebe8-7a0b-49ae-901e-c877f090a7fc'}
)
# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
FunctionsToExport = @(
'Get-COMDetails',
'Get-DNSNamesBySubnet',
'Get-ShortCutPath',
'New-ConsoleArgWriter',
'touch',
'tail',
'Search-Registry',
'Set-WindowFocus',
'Wait-Active'
)
# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.
AliasesToExport = @('which')
# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
PrivateData = @{
PSData = @{
# Tags applied to this module. These help with module discovery in online galleries.
Tags = @('Profile', 'utility', 'windows')
# A URL to the license for this module.
LicenseUri = 'https://gist.github.com/tsmarvin/3e9c1a092214fd8269cbe4c2170d49a6#file-license'
# A URL to the main website for this project.
ProjectUri = 'https://gist.github.com/tsmarvin/3e9c1a092214fd8269cbe4c2170d49a6'
# Prerelease string of this module
# Prerelease = ''
# Flag to indicate whether the module requires explicit user acceptance for install/update/save
RequireLicenseAcceptance = $false
}
}
}
using namespace System.Collections.Generic
using namespace System.IO
# Write verbose messages on import
if ((Get-PSCallStack)[1].Arguments -imatch 'Verbose=True') { $PSDefaultParameterValues['*:Verbose'] = $true }
if ($env:OS -ne 'Windows_NT') {
Write-Verbose 'TM-WindowsUtility should only be run on windows.'
# Do not export any module commands.
Export-ModuleMember
return
}
# https://github.com/PowerShell/PowerShell/issues/17730#issuecomment-1190678484
$ExportedMembers = [List[string]]::new()
$ExportedAliases = [List[string]]::new()
Set-Alias -Name 'which' -Value 'where.exe'
$ExportedAliases.Add('which')
function Get-COMDetails {
<#
.SYNOPSIS
Gets extended COM file properties.
.DESCRIPTION
This function retrieves the first 1000 extended file properties using COM (Component Object Model) and returns
the Key/Value pairs in a PSCustomObject.
.PARAMETER File
The FileInfo object that has extended properties to analyze.
#>
[CmdletBinding()]
[OutputType([PSCustomObject])]
param (
[Parameter(Mandatory, ValueFromPipeline)]
[Alias('Path', 'PSPath')]
[Validation.ValidatePathExists('File')]
[FileInfo]$File
)
begin { $Output = [PSCustomObject]@{ FullName = $File.FullName } }
process {
$objShell = New-Object -ComObject Shell.Application
$objFolder = $objShell.Namespace($File.DirectoryName)
$objFile = $objFolder.ParseName($File.Name)
if ($objFile) {
for ($ColumnNumber = 0; $ColumnNumber -le 1000; $ColumnNumber++) {
$ColumnName = $null
$ColumnValue = $null
$ColumnName = $objFolder.GetDetailsOf($null, $ColumnNumber)
$ColumnValue = $objFolder.GetDetailsOf($ObjFile, $ColumnNumber)
if ($ColumnValue) {
# Ensure we always have a property name.
if (-Not $ColumnName) { $ColumnName = "ColumnNumber$ColumnNumber" }
$Output.PSObject.Properties.Add( [PSNoteProperty]::New($ColumnName, $ColumnValue) )
}
}
}
}
end { return $Output }
}
$ExportedMembers.Add('Get-COMDetails')
function Get-DNSNamesBySubnet {
<#
.SYNOPSIS
Attempts to retrieve DNS names for devices active within in a specified subnet.
.DESCRIPTION
This function queries DNS names for devices in a specified subnet and returns a list of objects
containing their IPAddress, DeviceName, and ping statuses.
.PARAMETER Subnet
The subnet to query for DNS names. Should be a valid IP address.
.PARAMETER CIDR
The CIDR value for the subnet (default is 24). Acceptable values are between 24 and 30.
.PARAMETER PassThru
A switch that indicates that the output should be returned as a list of objects instead of a formatted table.
.PARAMETER IncludeAllResults
A switch that includes all results, including for IPs with empty DNS names.
#>
[CmdletBinding()]
[OutputType([void], [PSCustomObject])]
param (
[Parameter(
Position = 0,
Mandatory,
ValueFromPipeline,
ValueFromPipelineByPropertyName
)]
[Validation.ValidateIPv4Format()]
[string]$IPV4Address,
[Parameter(
Position = 1,
Mandatory = $false,
ValueFromPipeline
)]
[ValidateRange(24, 30)]
[int]$CIDR = 24,
[Parameter(Mandatory = $false)]
[switch]$PassThru,
[Parameter(Mandatory = $false)]
[switch]$IncludeAllResults
)
begin {
if ((Test-ApplicationExistsInPath -ApplicationName 'nslookup') -eq $false) {
throw 'Get-DNSNamesBySubnet requires nslookup to run but it is not available in the PATH.'
}
$FirstThreeOctets = $IPV4Address -replace "$($IPV4Address.Split('.')[-1])$", ''
[int]$FinalOctet = $IPV4Address -replace "^$FirstThreeOctets", ''
$Output = [List[object]]::New()
$CIDRAdressCount = switch ($CIDR) {
24 { 254 }
25 { 126 }
26 { 62 }
27 { 30 }
28 { 14 }
29 { 7 }
30 { 2 }
}
$End = $FinalOctet + $CIDRAdressCount
if ($End -gt 255) { $End = 255 }
}
process {
$JobOutput = if (($env:OS -eq 'Windows_NT') -and (-Not $IsWindows)) {
$Jobs = for ([int]$i = $FinalOctet;$i -le $End;$i++){
Start-Job -Name "DNSSearch$i" -ScriptBlock {
param(
[int]$InputNumber,
[string]$FirstThreeOctets,
[int]$FinalOctet,
[bool]$IncludeAllResults
)
try {
$Address = $stdout = $DNSName = $null
$Address = "$FirstThreeOctets$($FinalOctet + $InputNumber)"
$null = . { nslookup $Address | Set-Variable stdout } 2>&1 | ForEach-Object ToString
try {
$selectStringSplat = @{
InputObject = $stdout
Pattern = 'Name:'
SimpleMatch = $true
ErrorAction = 'Stop'
}
$DNSName = (Select-String @selectStringSplat)[0].line.split(':')[-1].trim()
} catch { }
if ($IncludeAllResults -or (-Not [string]::IsNullOrWhiteSpace($DNSName))) {
return [PSCustomObject]@{
FinalOctet = ($FinalOctet + $InputNumber)
IPAddress = $Address
DeviceName = $DNSName
Connection = try {(Test-Connection $Address -ErrorAction Stop)} catch { }
}
}
} catch {}
} -ArgumentList $i,$FirstThreeOctets,$FinalOctet,$IncludeAllResults
}
$Jobs | Receive-Job -Wait
} else {
$FinalOctet..$End | ForEach-Object -Parallel {
try {
$Address = $stdout = $DNSName = $null
$Address = "$using:FirstThreeOctets$($using:FinalOctet + $_)"
$null = . { nslookup $Address | Set-Variable stdout } 2>&1 | ForEach-Object ToString
try {
$selectStringSplat = @{
InputObject = $stdout
Pattern = 'Name:'
SimpleMatch = $true
ErrorAction = 'Stop'
}
$DNSName = (Select-String @selectStringSplat)[0].line.split(':')[-1].trim()
} catch { }
if ($using:IncludeAllResults -or (-Not [string]::IsNullOrWhiteSpace($DNSName))) {
return [PSCustomObject]@{
FinalOctet = ($using:FinalOctet + $_)
IPAddress = $Address
DeviceName = $DNSName
Connection = (Test-Connection $Address)
}
}
} catch { }
}
}
$JobOutput = $JobOutput | Sort-Object -Property FinalOctet
foreach ($Job in $JobOutput) {
$Output.Add(
[PSCustomObject]@{
IPAddress = $Job.IPAddress
DeviceName = $Job.DeviceName
Ping01 = $Job.Connection[0].status
Ping02 = $Job.Connection[1].status
Ping03 = $Job.Connection[2].status
Ping04 = $Job.Connection[3].status
}
)
}
}
end {
if ($Output.Length -eq 0) { Write-Host 'No DNS names found.' }
if ($PassThru) { return $Output } else { ($Output | Format-Table -Property *) }
}
}
$ExportedMembers.Add('Get-DNSNamesBySubnet')
function Get-ShortCutPath {
<#
.SYNOPSIS
Gets the path to the target of a windows shortcut (.lnk / .url) file.
.DESCRIPTION
Uses wscript to retrieve the TargetPath and Arguments, if set, of a windows shortcut (.lnk / .url) file.
.PARAMETER Path
The path(s) to the shortcut file to analyze.
#>
[CmdletBinding()]
[OutputType([System.String])]
param(
[Parameter(
Mandatory,
ValueFromPipeline,
ValueFromPipelineByPropertyName,
Position = 0
)]
[Alias('FullName')]
[string[]]$Path
)
begin { $wShell = New-Object -ComObject WScript.Shell }
process {
foreach ($Item in $Path){
$ItemPath = [IO.Path]::GetFullPath($Item)
$ItemExt = [IO.Path]::GetExtension($ItemPath)
$VerboseMessage = "Processing '$Item'"
if ($Item -ne $ItemPath) { $VerboseMessage += "as '$ItemPath'" }
Write-Verbose -Message "$VerboseMessage."
if ((Test-Path $ItemPath -PathType Leaf) -eq $false) {
Write-Error "Path '$ItemPath' is not a file that exists."
continue
} elseif (($ItemExt -ine '.lnk') -and ($ItemExt -ine '.url')) {
Write-Error "Path '$ItemPath' does not have the required '.lnk' or '.url' extension!"
continue
}
$shortCut = $wShell.CreateShortcut($ItemPath)
$Result = $shortCut.TargetPath
if ($shortCut.Arguments){ $Result += " $($shortCut.Arguments)" }
Write-Output $Result
}
}
}
$ExportedMembers.Add('Get-ShortCutPath')
function New-ConsoleArgWriter {
<#
.SYNOPSIS
Creates a new 'ConsoleArgWriter' console application if it does not already exist.
.DESCRIPTION
The New-ConsoleArgWriter function generates an executable, ConsoleArgWriter.exe, in the specified $ShellBinPath.
Use this executable as a mock for other executables when testing. ConsoleArgWriter.exe will write the input
arguments to the console, rather than executing the actual application. This is useful for testing and debugging
scripts that utilize command-line applications when you don't actually want to run the executables.
This function checks if the 'ConsoleArgWriter.exe' already exists in the specified path. If it doesn't,
it uses the Add-Type cmdlet to compile a new console application from a C# script. The new application
simply writes the provided arguments to the console.
To use the executable after running this function, alias the desired executable name to ConsoleArgWriter.exe.
For example:
Set-Alias -Name 'gpg.exe' -Value (Join-Path $ShellPath 'bin' 'ConsoleArgWriter.exe')
& 'gpg.exe' --pinentry-mode=loopback "--d=$true"
In this case, any arguments passed to 'gpg.exe' would be written to the console instead of being passed to the
actual gpg.exe executable.
.PARAMETER ShellBinPath
The path where the ConsoleArgWriter.exe will be created.
.EXAMPLE
New-ConsoleArgWriter -ShellBinPath C:\temp
This will create a new ConsoleArgWriter.exe in the C:\temp directory.
.NOTES
The New-ConsoleArgWriter function should be used for debugging and testing purposes. Avoid using it
in production environments as it could potentially expose sensitive command-line arguments.
#>
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[Validation.ValidatePathExists('Folder')]
[string]$ShellBinPath
)
$ArgWriter = Join-Path -Path $ShellBinPath -ChildPath 'ConsoleArgWriter.exe'
if ((Test-Path -Path $ArgWriter) -eq $false) {
if ($env:Path.Split([System.IO.Path]::PathSeparator) -notcontains $ShellBinPath) {
Write-Warning (
"The ShellBinPath '$ShellBinPath' is not in your `$Env:Path. To use the ConsoleArgWriter you will need " +
"to alias the entire path '$ArgWriter'."
)
}
# Starting this in Windows PowerShell because it can emit console applications.
$CreateConsoleWriter = ([hashtable]@{
FilePath = 'powershell.exe'
NoNewWindow = $true
WorkingDirectory = $ShellBinPath
ArgumentList = (
'-noprofile',
'-ec',
[Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes(@'
[hashtable]$ConsoleWriterSplat = @{
OutputType = 'ConsoleApplication'
Language = 'CSharp'
OutputAssembly = 'ConsoleArgWriter.exe'
TypeDefinition = @"
using System;
public class Program {
public static int Main(string[] args) {
int count = 0;
foreach (string arg in args) {
if (null == arg) {
Console.WriteLine("{0}: null", count);
} else {
Console.WriteLine("{0}: \"{1}\"", count, arg);
}
count++;
}
return 0;
}
}
"@
}
Add-Type @ConsoleWriterSplat
'@)))
})
Start-Process @CreateConsoleWriter
}
}
$ExportedMembers.Add('New-ConsoleArgWriter')
function touch {
<#
.SYNOPSIS
Create a new file at the specified path.
.DESCRIPTION
The touch function emulates the Unix touch command and creates a new file at
the specified path if it does not already exist.
.PARAMETER Path
The path where the new file will be created.
#>
[CmdletBinding()]
[OutputType([Void])]
param (
[Parameter(
Mandatory,
ValueFromPipeline,
Position = 0
)]
[string]$Path
)
process {
New-Item -ItemType File -Path $Path
}
}
$ExportedMembers.Add('touch')
function tail {
<#
.SYNOPSIS
Display the last 'n' lines of a file.
.DESCRIPTION
The tail function emulates the Unix tail command and displays the last 'n' lines of a file.
.PARAMETER Path
The path of the file to display.
.PARAMETER LiteralPath
The literal path of the file to display.
.PARAMETER follow
A switch that indicates that the function should follow the file as it grows.
.PARAMETER lines
The number of lines to display from the end of the file. Default is 10.
.EXAMPLE
# Use Control C to exit the function and stop tailing the file.
tail -f 'C:\Path\To\File.txt'
#>
[CmdletBinding()]
[OutputType([Void])]
param (
[Parameter(
Mandatory,
ValueFromPipeline,
ParameterSetName = 'Path',
Position = 0
)]
[Validation.ValidatePathExists('File')]
[FileInfo]$Path,
[Parameter(
Mandatory,
ValueFromPipeline,
ParameterSetName = 'LiteralPath',
Position = 0
)]
[Validation.ValidatePathExists('File')]
[string]$LiteralPath,
[Parameter(Mandatory = $false)]
[switch]$follow,
[Parameter(Mandatory = $false)]
[Alias('n')]
[int]$lines = 10
)
$ParameterSplat = @{}
if ($Path) { $ParameterSplat.Add('Path', $Path) }
if ($LiteralPath) { $ParameterSplat.Add('LiteralPath', $LiteralPath) }
if ($follow) { $ParameterSplat.Add('Wait', $true) }
$ParameterSplat.Add('Tail', $lines)
Get-Content @ParameterSplat
}
$ExportedMembers.Add('tail')
function Search-Registry {
<#
.SYNOPSIS
Search the Windows Registry for key names, key value names, or key value data matching specified criteria.
.DESCRIPTION
The Search-Registry function allows you to search the Windows Registry for
- Key names
- Value names
- Value data
It outputs PSCustomObjects that contain the key, matched content type (KeyName, ValueName, or ValueData), and the
matching content.
.PARAMETER Path
The registry path to start the search from.
.PARAMETER Recurse
Indicates whether to search in subkeys.
.PARAMETER SearchRegex
The regular expression used as the search criteria.
.PARAMETER KeyName
Used in conjunction with the SearchRegex parameter. Specifies that key names will be tested.
By default, when the SingleSearchString switches have not been specified, all content types are evaluated.
When using this switch, only the specified type(s) (KeyName, ValueName, and/or ValueData) will be evaluated.
.PARAMETER ValueName
Used in conjunction with the SearchRegex parameter. Specifies that the value names will be tested.
By default, when the SingleSearchString switches have not been specified, all content types are evaluated.
When using this switch, only the specified type(s) (KeyName, ValueName, and/or ValueData) will be evaluated.
.PARAMETER ValueData
Used in conjunction with the SearchRegex parameter. Specifies that the value data will be tested.
By default, when the SingleSearchString switches have not been specified, all content types are evaluated.
When using this switch, only the specified type(s) (KeyName, ValueName, and/or ValueData) will be evaluated.
.PARAMETER KeyNameRegex
Specifies a regex that will be checked against key names only.
.PARAMETER ValueNameRegex
Specifies a regex that will be checked against value names only.
.PARAMETER ValueDataRegex
Specifies a regex that will be checked against value data only.
.EXAMPLE
# Search for key names matching "Microsoft," "Adobe," or "Google" under the specified registry path that has value
# data consisting of only numeric characters.
Search-Registry -Path HKLM:\SOFTWARE -KeyNameRegex "Microsoft|Adobe|Google" -ValueDataRegex "^\d+$"
.EXAMPLE
# Recursively search for value names matching "DNS*" or "IP*" under the given path, including subkeys.
Search-Registry -Path HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters -Recurse -ValueNameRegex "DNS*|IP*"
.EXAMPLE
# Search for key names containing the word "Security" under the specified registry path.
Search-Registry -Path HKCU:\Software -KeyNameRegex "Security"
.EXAMPLE
# Search for keys with empty value data (no data) under the specified registry path.
Search-Registry -Path HKCU:\Software -ValueDataRegex "^$"
.EXAMPLE
# Search for value names containing "Program Files" under the specified registry path.
Search-Registry -Path HKLM:\SOFTWARE -ValueNameRegex "Program Files"
.EXAMPLE
# Find key names that match "ProgramKeyName". This will not evaluate either ValueNames or ValueData.
Search-Registry -Path HKLM:\SOFTWARE -SearchRegex "ProgramKeyName" -KeyName
#>
[CmdletBinding()]
param (
[Alias('PsPath')]
[Parameter(Mandatory, Position = 0, ValueFromPipelineByPropertyName)]
[string[]]$Path,
[Parameter(Mandatory = $false)]
[switch]$Recurse,
[Parameter(ParameterSetName = 'SingleSearchString', Mandatory)]
[string]$SearchRegex,
[Parameter(ParameterSetName = 'SingleSearchString', Mandatory = $false)]
[switch]$KeyName,
[Parameter(ParameterSetName = 'SingleSearchString', Mandatory = $false)]
[switch]$ValueName,
[Parameter(ParameterSetName = 'SingleSearchString', Mandatory = $false)]
[switch]$ValueData,
[Parameter(ParameterSetName = 'MultipleSearchStrings', Mandatory = $false)]
[string]$KeyNameRegex,
[Parameter(ParameterSetName = 'MultipleSearchStrings', Mandatory = $false)]
[string]$ValueNameRegex,
[Parameter(ParameterSetName = 'MultipleSearchStrings', Mandatory = $false)]
[string]$ValueDataRegex
)
begin {
if ($PSCmdlet.ParameterSetName -eq 'SingleSearchString') {
$NoSwitchesSpecified = $false -eq (
$PSBoundParameters.ContainsKey('KeyName') -or
$PSBoundParameters.ContainsKey('ValueName') -or
$PSBoundParameters.ContainsKey('ValueData')
)
if ($KeyName -or $NoSwitchesSpecified) { $KeyNameRegex = $SearchRegex }
if ($ValueName -or $NoSwitchesSpecified) { $ValueNameRegex = $SearchRegex }
if ($ValueData -or $NoSwitchesSpecified) { $ValueDataRegex = $SearchRegex }
}
}
process {
foreach ($CurrentPath in $Path) {
foreach ($Key in (Get-ChildItem $CurrentPath -Recurse:$Recurse)) {
if ($KeyNameRegex) {
Write-Verbose "$($Key.Name): Checking KeyNamesRegex"
if ($Key.PSChildName -match $KeyNameRegex) {
Write-Verbose "$($Key.Name): Found KeyName match for `"$KeyNameRegex`"!"
Write-Output -InputObject ([PSCustomObject]@{
Key = $Key
ContentMatch = 'KeyName'
MatchContent = $Key.PSChildName
}
)
}
}
if ($ValueNameRegex -or $ValueDataRegex) {
foreach ($KeyValueName in $Key.GetValueNames()) {
if ($ValueNameRegex) {
Write-Verbose "$($Key.Name): Checking ValueNamesRegex"
if ($KeyValueName -match $ValueNameRegex) {
Write-Verbose "$($Key.Name): Found ValueName match for `"$ValueNameRegex`"!"
Write-Output -InputObject ([PSCustomObject]@{
Key = $Key
ContentMatch = 'ValueName'
MatchContent = $KeyValueName
}
)
}
}
if ($ValueDataRegex) {
Write-Verbose "$($Key.Name): Checking ValueDataRegex"
$KeyValueData = $Key.GetValue($KeyValueName)
if ($KeyValueData -match $ValueDataRegex) {
Write-Verbose "$($Key.Name): Found ValueData match for `"$ValueDataRegex`"!"
Write-Output -InputObject ([PSCustomObject] @{
Key = $Key
ContentMatch = 'ValueData'
MatchContent = $KeyValueData
}
)
}
}
}
}
}
}
}
}
$ExportedMembers.Add('Search-Registry')
function Set-WindowFocus {
<#
.SYNOPSIS
Sets the focus on a specified window.
.DESCRIPTION
This function shifts focus to a specified window by utilizing either the process name or the process object.
By invoking Windows APIs, the function manages the visibility to show or hide GUI process windows.
.PARAMETER ProcessName
The name of the process whose window you want to focus.
.PARAMETER Process
The process object whose window you want to focus.
.PARAMETER Minimize
A switch that indicates if the window should be minimized after setting the focus.
#>
[CmdletBinding()]
[OutputType([Void])]
param (
[Parameter(
Mandatory,
ValueFromPipeline,
ParameterSetName = 'ProcessName',
Position = 0
)]
[string]$ProcessName,
[Parameter(
Mandatory,
ValueFromPipeline,
ParameterSetName = 'Process',
Position = 0
)]
[ComponentModel.Component]$Process,
[Parameter(Mandatory = $false)]
[switch]$Minimize
)
# https://stackoverflow.com/a/58548853
Add-Type -Namespace ProfileUtility -Name WindowVisibility -MemberDefinition @'
[DllImport("user32.dll", SetLastError=true)]
public static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll", SetLastError=true)]
public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll", SetLastError=true)]
public static extern bool IsIconic(IntPtr hWnd);
'@
switch ($PSCmdlet.ParameterSetName) {
'ProcessName' {
$ProcessName = $ProcessName -replace '\.exe$'
$Processes = (Get-Process -ErrorAction Ignore $ProcessName)
if ($Processes.Count -gt 1) {
$Index = 0
$ProcessList = Foreach ($Process in $Processes) {
[PSCustomObject]@{
Number = $Index
WindowTitle = $Process.MainWindowTitle
}
++$Index
}
$PromptAnswer = Read-Host -Prompt (
"Found multiple processes for '$ProcessName'. Which would you like to select.`n" +
($ProcessList | Format-Table | Out-String)
)
$hWnd = $Processes[$PromptAnswer].MainWindowHandle
} else {
try { $hWnd = $Processes[0].MainWindowHandle } catch { <# Do Nothing #> }
}
}
'Process' { $hWnd = $Process.MainWindowHandle }
}
if (-not $hWnd) { Throw 'Failed to retrieve MainWindowHandle.' }
# https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow
if ($Minimize) {
[ProfileUtility.WindowVisibility]::ShowWindow($hwnd, 6) | Out-Null
} else {
# Set the focus on the window.
[ProfileUtility.WindowVisibility]::SetForegroundWindow($hWnd) | Out-Null
# If the window is minimized, restore it.
if ([ProfileUtility.WindowVisibility]::IsIconic($hwnd)) {
[ProfileUtility.WindowVisibility]::ShowWindow($hwnd, 9) | Out-Null
}
}
}
$ExportedMembers.Add('Set-WindowFocus')
function Wait-Active {
<#
.SYNOPSIS
Keeps the computer active for a specified amount of time.
.DESCRIPTION
This function opens Notepad and sends a key press to keep the computer active.
The duration can be specified in minutes, as a TimeSpan, or as a DateTime.
.PARAMETER StopTime
The DateTime at which the function should stop keeping the computer active.
.PARAMETER Duration
The TimeSpan duration to keep the computer active.
.PARAMETER Minutes
The number of minutes to keep the computer active.
.PARAMETER SleepSeconds
The number of seconds to sleep between each activity (default is 60 seconds).
#>
[CmdletBinding(DefaultParameterSetName = 'Minutes')]
[OutputType([Void])]
param (
[Parameter(
Position = 0,
ParameterSetName = 'DateTime',
Mandatory = $false,
ValueFromPipeline,
ValueFromPipelineByPropertyName
)]
[DateTime]$StopTime = (Get-Date).AddMinutes(5),
[Parameter(
Position = 0,
ParameterSetName = 'TimeSpan',
Mandatory = $false,
ValueFromPipeline,
ValueFromPipelineByPropertyName
)]
[ValidateRange(0, [int]::MaxValue)]
[TimeSpan]$Duration = (New-TimeSpan -Start (Get-Date) -End (Get-Date).AddMinutes(5)),
[Parameter(
Position = 0,
ParameterSetName = 'Minutes',
Mandatory = $false,
ValueFromPipeline,
ValueFromPipelineByPropertyName
)]
[ValidateRange(0, [int]::MaxValue)]
[int]$Minutes = 5,
[Parameter(
Position = 1,
Mandatory = $false,
ValueFromPipelineByPropertyName
)]
[ValidateRange(0, [int]::MaxValue)]
[Int]$SleepSeconds = 60
)
$WaitTill = switch ($PSCmdlet.ParameterSetName) {
'DateTime' { $StopTime }
'TimeSpan' { (Get-Date).AddSeconds($timespan.TotalSeconds) }
'Minutes' { (Get-Date).AddMinutes($Minutes) }
}
$Process = Start-Process 'Notepad.exe' -PassThru
Start-Sleep -Seconds 5
do {
if ((Get-Process -InputObject $Process).HasExited) {
Write-Warning 'Notepad has exited.'
return
}
Set-WindowFocus $Process
(New-Object -ComObject wscript.shell).SendKeys('.')
Set-WindowFocus $Process -Minimize
Start-Sleep -Seconds $SleepSeconds
$CurrentTime = Get-Date
} while ($CurrentTime -lt $WaitTill)
Stop-Process $Process
}
$ExportedMembers.Add('Wait-Active')
Export-ModuleMember -Function $ExportedMembers.ToArray() -Alias $ExportedAliases.ToArray()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment