I'm working on a script for use in Windows 7 and Windows 10 for a Windows Registry search in HKLM:\Software\Classes. So far my code works, but it's extremely slow. It takes about 30 minutes to complete.
I need to use Set-Location also to avoid an error with Get-ItemProperty, which occurs because the $path is not a valid object.
How can I speed this code up? What's wrong?
File regsearch.ps1 (Mathias R. Jessen's answer applied)
Function Get-RegItems
{
Param(
[Parameter(Mandatory=$true)]
[string]$path,
[string]$match)
#Set Local Path and ignore wildcard (literalpath)
Set-Location -literalpath $path
$d = Get-Item -literalpath $path
# If more than one value -> process
If ($d.Valuecount -gt 0) {
$d |
# Get unkown property
Select-Object -ExpandProperty Property |
ForEach {
$val = (Get-ItemProperty -Path . -Name $_).$_
#if Filter $match found, generate ReturnObject
if (($_ -match $match) -or ($val -match $match ) -or ($path-match $match)) {
New-Object psobject -Property #{ “key”=$path; “property”=$_; “value” = $val ;}
}
}
}
} #end function Get-RegItems
Function RegSearch
{
Param(
[Parameter(Mandatory=$true)]
[string]$path,
[string]$match)
# Expand $path if necessary to get a valid object
if ($path.Indexof("HKEY") -ne "-1" -and $path.Indexof("Registry::") -eq "-1" ) {
$path = "Microsoft.PowerShell.Core\Registry::" +$path
}
# Retrieve items of the main key
Get-RegItems -path $path -match $match
# Retrieve items of all child keys
Get-ChildItem $path -Recurse -ErrorAction SilentlyContinue |
ForEach {
Get-RegItems -path $_.PsPath -match $match
}
} #end function RegSearch
#$search = "HKCU:\SOFTWARE\Microsoft\Office"
$searchkey = ‘HKLM:\SOFTWARE\Microsoft\Office\’
#$searchkey = "HKLM:\Software\Classes\"
$pattern = "EventSystem"
cls
$result = #()
Measure-Command {$result = Regsearch -path $searchkey -match $pattern }
# TESTING
#$t = #( "Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\Software\Classes",
# "HKLM:\Software\Classes\Wow6432Node\CLSID\",
# "HKCU:\SOFTWARE\Microsoft\Office\",
# "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Office")
#cls
#$t |ForEach { Get-RegItems -path $_ } | fl
if ($result.Count) {
$result
"Count: {0}" -f ($result.Count-1)
}
else {
"Path: {0} `nNo Items found" -f $searchkey
}
I accepted the challenge and made it "as fast as possible".
Now it is even faster than REGEDIT or any other tool.
The below sample lasts 11 seconds to parse the complete OFFICE-key and all subkeys.
In addition, it also searches for string-matches in REG-BINARY etc.
Enjoy!
# carsten.giese#googlemail.com
# reference: https://msdn.microsoft.com/de-de/vstudio/ms724875(v=vs.80)
cls
remove-variable * -ea 0
$ErrorActionPreference = "stop"
$signature = #'
[DllImport("advapi32.dll")]
public static extern Int32 RegOpenKeyEx(
UInt32 hkey,
StringBuilder lpSubKey,
int ulOptions,
int samDesired,
out IntPtr phkResult
);
[DllImport("advapi32.dll")]
public static extern Int32 RegQueryInfoKey(
IntPtr hKey,
StringBuilder lpClass, Int32 lpCls, Int32 spare,
out int subkeys, out int skLen, int mcLen, out int values,
out int vNLen, out int mvLen, int secDesc,
out System.Runtime.InteropServices.ComTypes.FILETIME lpftLastWriteTime
);
[DllImport("advapi32.dll", CharSet = CharSet.Unicode)]
public static extern Int32 RegEnumValue(
IntPtr hKey,
int dwIndex,
IntPtr lpValueName,
ref IntPtr lpcchValueName,
IntPtr lpReserved,
out IntPtr lpType,
IntPtr lpData,
ref int lpcbData
);
[DllImport("advapi32.dll", CharSet = CharSet.Unicode)]
public static extern Int32 RegEnumKeyEx(
IntPtr hKey,
int dwIndex,
IntPtr lpName,
ref int lpcName,
IntPtr lpReserved,
IntPtr lpClass,
int lpcClass,
out System.Runtime.InteropServices.ComTypes.FILETIME lpftLastWriteTime
);
[DllImport("advapi32.dll")]
public static extern Int32 RegCloseKey(IntPtr hkey);
'#
$reg = add-type $signature -Name reg -Using System.Text -PassThru
$marshal = [System.Runtime.InteropServices.Marshal]
function search-RegistryTree($path) {
# open the key:
[IntPtr]$hkey = 0
$result = $reg::RegOpenKeyEx($global:hive, $path, 0, 25,[ref]$hkey)
if ($result -eq 0) {
# get details of the key:
$subKeyCount = 0
$maxSubKeyLen = 0
$valueCount = 0
$maxNameLen = 0
$maxValueLen = 0
$time = $global:time
$result = $reg::RegQueryInfoKey($hkey,$null,0,0,[ref]$subKeyCount,[ref]$maxSubKeyLen,0,[ref]$valueCount,[ref]$maxNameLen,[ref]$maxValueLen,0,[ref]$time)
if ($result -eq 0) {
$maxSubkeyLen += $maxSubkeyLen+1
$maxNameLen += $maxNameLen +1
$maxValueLen += $maxValueLen +1
}
# enumerate the values:
if ($valueCount -gt 0) {
$type = [IntPtr]0
$pName = $marshal::AllocHGlobal($maxNameLen)
$pValue = $marshal::AllocHGlobal($maxValueLen)
foreach ($index in 0..($valueCount-1)) {
$nameLen = $maxNameLen
$valueLen = $maxValueLen
$result = $reg::RegEnumValue($hkey, $index, $pName, [ref]$nameLen, 0, [ref]$type, $pValue, [ref]$valueLen)
if ($result -eq 0) {
$name = $marshal::PtrToStringUni($pName)
$value = switch ($type) {
1 {$marshal::PtrToStringUni($pValue)}
2 {$marshal::PtrToStringUni($pValue)}
3 {$b = [byte[]]::new($valueLen)
$marshal::Copy($pValue,$b,0,$valueLen)
if ($b[1] -eq 0 -and $b[-1] -eq 0 -and $b[0] -ne 0) {
[System.Text.Encoding]::Unicode.GetString($b)
} else {
[System.Text.Encoding]::UTF8.GetString($b)}
}
4 {$marshal::ReadInt32($pValue)}
7 {$b = [byte[]]::new($valueLen)
$marshal::Copy($pValue,$b,0,$valueLen)
$msz = [System.Text.Encoding]::Unicode.GetString($b)
$msz.TrimEnd(0).split(0)}
11 {$marshal::ReadInt64($pValue)}
}
if ($name -match $global:search) {
write-host "$path\$name : $value"
$global:hits++
} elseif ($value -match $global:search) {
write-host "$path\$name : $value"
$global:hits++
}
}
}
$marshal::FreeHGlobal($pName)
$marshal::FreeHGlobal($pValue)
}
# enumerate the subkeys:
if ($subkeyCount -gt 0) {
$subKeyList = #()
$pName = $marshal::AllocHGlobal($maxSubkeyLen)
$subkeyList = foreach ($index in 0..($subkeyCount-1)) {
$nameLen = $maxSubkeyLen
$result = $reg::RegEnumKeyEx($hkey, $index, $pName, [ref]$nameLen,0,0,0, [ref]$time)
if ($result -eq 0) {
$marshal::PtrToStringUni($pName)
}
}
$marshal::FreeHGlobal($pName)
}
# close:
$result = $reg::RegCloseKey($hkey)
# get Tree-Size from each subkey:
$subKeyValueCount = 0
if ($subkeyCount -gt 0) {
foreach ($subkey in $subkeyList) {
$subKeyValueCount += search-RegistryTree "$path\$subkey"
}
}
return ($valueCount+$subKeyValueCount)
}
}
$timer = [System.Diagnostics.Stopwatch]::new()
$timer.Start()
# setting global variables:
$search = "enterprise"
$hive = [uint32]"0x80000002" #HKLM
$subkey = "SOFTWARE\Microsoft\Office"
$time = New-Object System.Runtime.InteropServices.ComTypes.FILETIME
$hits = 0
write-host "We start searching for pattern '$search' in Registry-Path '$subkey' ...`n"
$count = search-RegistryTree $subkey
$timer.stop()
$sec = [int](100 * $timer.Elapsed.TotalSeconds)/100
write-host "`nWe checked $count reg-values in $sec seconds. Number of hits = $hits."
The single biggest improvement you can make here is changing:
Set-Location -literalpath $path
$d= Get-Item .
to
$d = Get-Item -LiteralPath $path
Manipulating the location stack for each key in the hierarchy introduces A LOT of unnecessary overhead
User function call overhead (scriptblocks included) is extremely big (e.g. 0.1-1ms). This becomes a very serious issue when the function is executed thousands/millions of times. Surprisingly, it's not mentioned in optimization-related articles (at least I've never seen it and I googled this topic a lot).
Unfortunately, the only only real solution to this particular issue is to inline the code at the cost of duplication and reduced readability.
Optimization should include code profiling.
PowerShell doesn't have a code profiler so you'll need to do it manually with Measure-Command.
Use System.Diagnostics.Stopwatch inside loops to display the accumulated time:
# global stopwatch
$sw1 = [Diagnostics.Stopwatch]::new()
$sw2 = [Diagnostics.Stopwatch]::new()
............
forEach(....) {
........
$sw1.start()
........
$sw1.stop()
........
$sw2.start()
........
$sw2.stop()
........
}
............
echo $sw1.ElapsedMilliseconds, $sw2.ElapsedMilliseconds
Here is a faster version of you sample-script.
Lasts ca. 1 minute on my machine.
If you need it faster, then you need to work with advapi32.dll-Pinvokes, but then
it will end quite complex.
Function Get-RegItems {
Param(
[Parameter(Mandatory=$true)]
[string]$path,
[string]$match
)
#write-host $path.Substring(30)
$key = Get-Item -literalpath $path
ForEach ($entry in $key.Property) {
$value = $key.GetValue($entry)
if (($entry -match $match) -or ($value -match $match ) -or ($path -match $match)) {
write-host "key=$path property=$entry value=$value"
}
}
}
Function RegSearch {
Param(
[Parameter(Mandatory=$true)]
[string]$path,
[string]$match
)
Get-RegItems -path $path -match $match
ForEach ($item in get-ChildItem -literalpath $path -ea 0) {
RegSearch -path $item.PsPath -match $match
}
}
cls
Remove-Variable * -ea 0
[System.GC]::Collect()
$searchkey =‘HKLM:\SOFTWARE\Microsoft\Office’
$pattern = "EventSystem"
measure-command {
$result = RegSearch -path $searchkey -match $pattern
}
Don't use the registry drive provider, if you want it faster.
I've also read classes with static methods are faster.
Related
when we open a windows explorer, we will see multi-column, like this:
now, I want to get the status column(Circled in red) value by PowerShell or java, Is there a way to do it?
You can include below function and modify as per your need in Powershell.
Below Powershell function can be used to get all details or properties of an item in file explorer.
function Get-FileMetaData {
<#
.SYNOPSIS
Small function that gets metadata information from file providing similar output to what Explorer shows when viewing file
.DESCRIPTION
Small function that gets metadata information from file providing similar output to what Explorer shows when viewing file
.PARAMETER File
FileName or FileObject
.EXAMPLE
Get-ChildItem -Path $Env:USERPROFILE\Desktop -Force | Get-FileMetaData | Out-HtmlView -ScrollX -Filtering -AllProperties
.EXAMPLE
Get-ChildItem -Path $Env:USERPROFILE\Desktop -Force | Where-Object { $_.Attributes -like '*Hidden*' } | Get-FileMetaData | Out-HtmlView -ScrollX -Filtering -AllProperties
.NOTES
#>
[CmdletBinding()]
param (
[Parameter(Position = 0, ValueFromPipeline)][Object] $File,
[switch] $Signature
)
Process {
foreach ($F in $File) {
$MetaDataObject = [ordered] #{}
if ($F -is [string]) {
$FileInformation = Get-ItemProperty -Path $F
} elseif ($F -is [System.IO.DirectoryInfo]) {
#Write-Warning "Get-FileMetaData - Directories are not supported. Skipping $F."
continue
} elseif ($F -is [System.IO.FileInfo]) {
$FileInformation = $F
} else {
Write-Warning "Get-FileMetaData - Only files are supported. Skipping $F."
continue
}
$ShellApplication = New-Object -ComObject Shell.Application
$ShellFolder = $ShellApplication.Namespace($FileInformation.Directory.FullName)
$ShellFile = $ShellFolder.ParseName($FileInformation.Name)
$MetaDataProperties = [ordered] #{}
0..400 | ForEach-Object -Process {
$DataValue = $ShellFolder.GetDetailsOf($null, $_)
$PropertyValue = (Get-Culture).TextInfo.ToTitleCase($DataValue.Trim()).Replace(' ', '')
if ($PropertyValue -ne '') {
$MetaDataProperties["$_"] = $PropertyValue
}
}
foreach ($Key in $MetaDataProperties.Keys) {
$Property = $MetaDataProperties[$Key]
$Value = $ShellFolder.GetDetailsOf($ShellFile, [int] $Key)
if ($Property -in 'Attributes', 'Folder', 'Type', 'SpaceFree', 'TotalSize', 'SpaceUsed') {
continue
}
If (($null -ne $Value) -and ($Value -ne '')) {
$MetaDataObject["$Property"] = $Value
}
}
if ($FileInformation.VersionInfo) {
$SplitInfo = ([string] $FileInformation.VersionInfo).Split([char]13)
foreach ($Item in $SplitInfo) {
$Property = $Item.Split(":").Trim()
if ($Property[0] -and $Property[1] -ne '') {
$MetaDataObject["$($Property[0])"] = $Property[1]
}
}
}
$MetaDataObject["Attributes"] = $FileInformation.Attributes
$MetaDataObject['IsReadOnly'] = $FileInformation.IsReadOnly
$MetaDataObject['IsHidden'] = $FileInformation.Attributes -like '*Hidden*'
$MetaDataObject['IsSystem'] = $FileInformation.Attributes -like '*System*'
if ($Signature) {
$DigitalSignature = Get-AuthenticodeSignature -FilePath $FileInformation.Fullname
$MetaDataObject['SignatureCertificateSubject'] = $DigitalSignature.SignerCertificate.Subject
$MetaDataObject['SignatureCertificateIssuer'] = $DigitalSignature.SignerCertificate.Issuer
$MetaDataObject['SignatureCertificateSerialNumber'] = $DigitalSignature.SignerCertificate.SerialNumber
$MetaDataObject['SignatureCertificateNotBefore'] = $DigitalSignature.SignerCertificate.NotBefore
$MetaDataObject['SignatureCertificateNotAfter'] = $DigitalSignature.SignerCertificate.NotAfter
$MetaDataObject['SignatureCertificateThumbprint'] = $DigitalSignature.SignerCertificate.Thumbprint
$MetaDataObject['SignatureStatus'] = $DigitalSignature.Status
$MetaDataObject['IsOSBinary'] = $DigitalSignature.IsOSBinary
}
[PSCustomObject] $MetaDataObject
}
}
}
Reference :
Follow Below Link for the complete tutorial.
https://evotec.xyz/getting-file-metadata-with-powershell-similar-to-what-windows-explorer-provides/
I am trying to modify my PowerShell script to find the best possible ways to check for Pending Reboots on our servers. This script checks the registry entries. However, I am seeing inconsistencies from other PowerShell scripts and wanting guidance on the best approach.
function PendingReboot ($comp) {
process {
try {
$WMI_OS = ""
$RegCon = ""
$WMI_OS = Get-WmiObject -Class Win32_OperatingSystem -ComputerName $comp -ErrorAction Stop
if ($?){
try{
$RegCon = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey([Microsoft.Win32.RegistryHive]"LocalMachine",$comp)
If ($WMI_OS.BuildNumber -ge 6001){
$RegValueSetupex = ""
$RegValuePFRO2k8 = ""
$RegSubKeySM = $RegCon.OpenSubKey("SYSTEM\CurrentControlSet\Control\Session Manager\")
$RegValueSetupex = $RegSubKeySM.GetValue("SetupExecute",$null)
if ($RegValueSetupex){
$RegValueSetupex = $true
}
$RegSubKeySM = $RegCon.OpenSubKey("SYSTEM\CurrentControlSet\Control\Session Manager\")
$RegValuePFRO2k8 = $RegSubKeySM.GetValue("PendingFileRenameOperations",$null)
if ($RegValuePFRO2k8 ){
$RegValuePFRO2k8 = $true
}
$RegCon.Close()
if ( $RegValueSetupex -eq $true -or $RegValuePFRO2k8 -eq $true){
return '<font color="#FF0000">'+$true
}
else {
return $false
}
}
else{
$RegValuePFRO2k3 = $false;
$RegCon = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey([Microsoft.Win32.RegistryHive]"LocalMachine","$comp")
$RegSubKeySM = $RegCon.OpenSubKey("SYSTEM\CurrentControlSet\Control\Session Manager\")
$RegValuePFRO2k3 = $RegSubKeySM.GetValue("PendingFileRenameOperations",$null)
$RegCon.Close()
If ($RegValuePFRO2k3) {
return '<font color="#FF0000">'+$true;
}
else {
return $false;
}
}
}
catch {
return '<font color="#FFFF00">'+"Remote Registry Service KO"
}
}
else {
throw $error[0].Exception
}
}
catch {
return '<font color="#FF0000">'+"RPC Issue"
}
}
}
Try this.
function PendingBoot($comp) {
$pendingRebootTests = #(
#{
Name = 'RebootPending'
Test = { Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing' Name 'RebootPending' -ErrorAction Ignore }
TestType = 'ValueExists'
}
#{
Name = 'RebootRequired'
Test = { Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update' Name 'RebootRequired' -ErrorAction Ignore }
TestType = 'ValueExists'
}
#{
Name = 'PendingFileRenameOperations'
Test = { Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager' -Name 'PendingFileRenameOperations' -ErrorAction Ignore }
TestType = 'NonNullValue'
}
)
$session = New-PSSession -Computer SRV1
foreach ($test in $pendingRebootTests) {
$result = Invoke-Command -Session $session -ScriptBlock $test.Test
if ($test.TestType -eq 'ValueExists' -and $result) {
$true
} elseif ($test.TestType -eq 'NonNullValue' -and $result -and $result.($test.Name)) {
$true
} else {
$false
}
}
$session | Remove-PSSession
}
Below is my script istalling Monserrat fonts from zip file. I can't figure how to check if a font already installed. After installation I can open folder C:\Windows\Fonts\Montserrat and I see al of them. When I am running script second time, it is not recognize existance of this folder. Where is my mistake?
$Source = "Montserrat.zip"
$FontsFolder = "FontMontserrat"
Expand-Archive $Source -DestinationPath $FontsFolder
$FONTS = 0x14
$CopyOptions = 4 + 16;
$objShell = New-Object -ComObject Shell.Application
$objFolder = $objShell.Namespace($FONTS)
$allFonts = dir $FontsFolder
foreach($File in $allFonts)
{
If((Test-Path "C:\Windows\Fonts\Montserrat") -eq $True)
{
echo "Font $File already installed"
}
Else
{
echo "Installing $File"
$CopyFlag = [String]::Format("{0:x}", $CopyOptions);
$objFolder.CopyHere($File.fullname,$CopyFlag)
}
}
Finally my script:
$Source = "Montserrat.zip"
$FontsFolder = "FontMontserrat"
Expand-Archive $Source -DestinationPath $FontsFolder -Force
$FONTS = 0x14
$CopyOptions = 4 + 16;
$objShell = New-Object -ComObject Shell.Application
$objFolder = $objShell.Namespace($FONTS)
$allFonts = dir $FontsFolder
foreach($font in Get-ChildItem -Path $fontsFolder -File)
{
$dest = "C:\Windows\Fonts\$font"
If(Test-Path -Path $dest)
{
echo "Font $font already installed"
}
Else
{
echo "Installing $font"
$CopyFlag = [String]::Format("{0:x}", $CopyOptions);
$objFolder.CopyHere($font.fullname,$CopyFlag)
}
}
I am running this script by following cmd:
set batchPath=%~dp0
powershell.exe -noexit -file "%batchPath%InstMontserrat.ps1"
I don't have to run it as administrator, but user have admin permissions.
Corrections of your script based on my comment assuming Windows 10:
# well-known SID for admin group
if ('S-1-5-32-544' -notin [System.Security.Principal.WindowsIdentity]::GetCurrent().Groups) {
throw 'Script must run as admin!'
}
$source = 'Montserrat.zip'
$fontsFolder = 'FontMontserrat'
Expand-Archive -Path $source -DestinationPath $fontsFolder
foreach ($font in Get-ChildItem -Path $fontsFolder -File) {
$dest = "C:\Windows\Fonts\$font"
if (Test-Path -Path $dest) {
"Font $font already installed."
}
else {
$font | Copy-Item -Destination $dest
}
}
If you do not want to install the font on OS level but only make it available for programs to use until reboot you may want to use this script that:
Will fail/throw if it cannot register/unregister font.
Broadcasts WM_FONTCHANGE to inform all windows that fonts have changed
Does not require administrator privileges
Does not install fonts in Windows, only makes them available for all programs in current session until reboot
Has verbose mode for debugging
Does not work with font folders
Usage:
register-fonts.ps1 [-v] [-unregister <PATH>[,<PATH>...]] [-register <PATH>[,<PATH>...]] # Register and unregister at same time
register-fonts.ps1 [-v] -unregister <PATH>
register-fonts.ps1 [-v] -register <PATH>
register-fonts.ps1 [-v] <PATH> # Will register font path
Param (
[Parameter(Mandatory=$False)]
[String[]]$register,
[Parameter(Mandatory=$False)]
[String[]]$unregister
)
# Stop script if command fails https://stackoverflow.com/questions/9948517/how-to-stop-a-powershell-script-on-the-first-error
$ErrorActionPreference = "Stop"
add-type -name Session -namespace "" -member #"
[DllImport("gdi32.dll")]
public static extern bool AddFontResource(string filePath);
[DllImport("gdi32.dll")]
public static extern bool RemoveFontResource(string filePath);
[return: MarshalAs(UnmanagedType.Bool)]
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern bool PostMessage(IntPtr hWnd, int Msg, int wParam = 0, int lParam = 0);
"#
$broadcast = $False;
Foreach ($unregisterFontPath in $unregister) {
Write-Verbose "Unregistering font $unregisterFontPath"
# https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-removefontresourcea
$success = [Session]::RemoveFontResource($unregisterFontPath)
if (!$success) {
Throw "Cannot unregister font $unregisterFontPath"
}
$broadcast = $True
}
Foreach ($registerFontPath in $register) {
Write-Verbose "Registering font $registerFontPath"
# https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-addfontresourcea
$success = [Session]::AddFontResource($registerFontPath)
if (!$success) {
Throw "Cannot register font $registerFontPath"
}
$broadcast = $True
}
if ($broadcast) {
# HWND_BROADCAST https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-postmessagea
$HWND_BROADCAST = New-Object IntPtr 0xffff
# WM_FONTCHANGE https://learn.microsoft.com/en-us/windows/win32/gdi/wm-fontchange
$WM_FONTCHANGE = 0x1D
Write-Verbose "Broadcasting font change"
# Broadcast will let other programs know that fonts were changed https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-postmessagea
$success = [Session]::PostMessage($HWND_BROADCAST, $WM_FONTCHANGE)
if (!$success) {
Throw "Cannot broadcase font change"
}
}
The script was inspired by this gist https://gist.github.com/Jaykul/d53a16ce5e7d50b13530acb4f98aaabd
'I am trying to copy multiple files from multiple folders into another folder. The folders have a part of the file name in them.
For example -
I want to copy all the files that have the name "Phone" or "cell" and the serial number as a part of the file name. Each sub folder has the serial number as a part of the folder name.
C:\shared\112\products\112.phone blah blah.txt
C:\shared\112\products\112.my cell.txt
C:\shared\113\products\113.ugly phone.txt
C:\shared\113\products\113.the cell.txt
C:\shared\114\products\114.pretty phone.txt
C:\shared\115\products\115.phone lazy.txt
C:\shared\115\products\115.celly cell.txt
The problem is there are 20,000 serial numbers so I want to set up a list of serial numbers and pull the file based on a set of serial number.
Here is my script but it is not pulling anything.'
$FirstSearchlist = #(“112”, “113”)
$SecondSearchlist = #("phone","cell")
$dirArray = #("c:\Shared\")
$NotFound = "Not Found"
cls
function Recurse([string]$path) {
$fc = new-object -com scripting.filesystemobject
$folder = $fc.getfolder($path)
foreach ($i in $folder.files) {
[string]$FullPath = $i.Path
[string]$FileName = $i.Name
foreach($first in $FirstSearchlist) {
if ($filename.ToUpper().Contains($first.ToUpper())) {
foreach($second in $SecondSearchlist) {
if ($filename.ToUpper().Contains($second.ToUpper())) {
Write-Host $Fullpath
Copy-Item $Fullpath -Destination "C:\Shared\Phones" -Recurse
}
}
}
}
}
foreach ($i in $folder.subfolders) {
Recurse($i.path)
}
}
function main() {
foreach ($i in $FirstSearchlist){
$NewFolder = $dirArray + $i
foreach($SearchPath in $NewFolder) {
Recurse $SearchPath
}
}
}
main
This worked for me testing with the example set provided (but note in my testing C:\Shared is empty, may want to adjust down your folder tree depending):
Function Get-FileMatches($Filter){
$fileMatches = Get-ChildItem -Path "C:\Shared" -Recurse | ? {$_.Mode -notmatch 'd' -and $_.Name -match $Filter -and ($_.Name -match $SecondSearchlist[0] -or $_.Name -match $SecondSearchlist[1])}
return $fileMatches
}
Function Run-Script() {
$FirstSearchlist = #(“112”, “113”)
$SecondSearchlist = #("phone","cell")
$allMatches = #()
$phonesFolderExists = Test-Path "C:\Shared\Phones"
if($phonesFolderExists -eq $False)
{
New-Item -Path "C:\Shared\Phones" -ItemType Directory -Force
}
foreach($listItem in $FirstSearchList) {
$currentMatches = Get-FileMatches -Filter $listItem
if($currentMatches -ne $null)
{
foreach($item in $currentMatches)
{
$allMatches += $item
}
}
}
if($allMatches -ne $null)
{
foreach($item in $allMatches)
{
Copy-Item -Path $item.FullName -Destination "C:\Shared\Phones"
}
}
}
Run-Script
What is the easiest way to convert a PSCustomObject to a Hashtable? It displays just like one with the splat operator, curly braces and what appear to be key value pairs. When I try to cast it to [Hashtable] it doesn't work. I also tried .toString() and the assigned variable says its a string but displays nothing - any ideas?
Shouldn't be too hard. Something like this should do the trick:
# Create a PSCustomObject (ironically using a hashtable)
$ht1 = #{ A = 'a'; B = 'b'; DateTime = Get-Date }
$theObject = new-object psobject -Property $ht1
# Convert the PSCustomObject back to a hashtable
$ht2 = #{}
$theObject.psobject.properties | Foreach { $ht2[$_.Name] = $_.Value }
Keith already gave you the answer, this is just another way of doing the same with a one-liner:
$psobject.psobject.properties | foreach -begin {$h=#{}} -process {$h."$($_.Name)" = $_.Value} -end {$h}
Here's a version that works with nested hashtables / arrays as well (which is useful if you're trying to do this with DSC ConfigurationData):
function ConvertPSObjectToHashtable
{
param (
[Parameter(ValueFromPipeline)]
$InputObject
)
process
{
if ($null -eq $InputObject) { return $null }
if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string])
{
$collection = #(
foreach ($object in $InputObject) { ConvertPSObjectToHashtable $object }
)
Write-Output -NoEnumerate $collection
}
elseif ($InputObject -is [psobject])
{
$hash = #{}
foreach ($property in $InputObject.PSObject.Properties)
{
$hash[$property.Name] = ConvertPSObjectToHashtable $property.Value
}
$hash
}
else
{
$InputObject
}
}
}
My extremely lazy approach, enabled by a new feature in PowerShell 6:
$myhashtable = $mypscustomobject | ConvertTo-Json | ConvertFrom-Json -AsHashTable
This works for PSCustomObjects created by ConvertFrom_Json.
Function ConvertConvertFrom-JsonPSCustomObjectToHash($obj)
{
$hash = #{}
$obj | Get-Member -MemberType Properties | SELECT -exp "Name" | % {
$hash[$_] = ($obj | SELECT -exp $_)
}
$hash
}
Disclaimer: I barely understand PowerShell so this is probably not as clean as it could be. But it works (for one level only).
My code:
function PSCustomObjectConvertToHashtable() {
param(
[Parameter(ValueFromPipeline)]
$object
)
if ( $object -eq $null ) { return $null }
if ( $object -is [psobject] ) {
$result = #{}
$items = $object | Get-Member -MemberType NoteProperty
foreach( $item in $items ) {
$key = $item.Name
$value = PSCustomObjectConvertToHashtable -object $object.$key
$result.Add($key, $value)
}
return $result
} elseif ($object -is [array]) {
$result = [object[]]::new($object.Count)
for ($i = 0; $i -lt $object.Count; $i++) {
$result[$i] = (PSCustomObjectConvertToHashtable -object $object[$i])
}
return ,$result
} else {
return $object
}
}
For simple [PSCustomObject] to [Hashtable] conversion Keith's Answer works best.
However if you need more options you can use
function ConvertTo-Hashtable {
<#
.Synopsis
Converts an object to a hashtable
.DESCRIPTION
PowerShell v4 seems to have trouble casting some objects to Hashtable.
This function is a workaround to convert PS Objects to [Hashtable]
.LINK
https://github.com/alainQtec/.files/blob/main/src/scripts/Converters/ConvertTo-Hashtable.ps1
.NOTES
Base ref: https://community.idera.com/database-tools/powershell/powertips/b/tips/posts/turning-objects-into-hash-tables-2
#>
PARAM(
# The object to convert to a hashtable
[Parameter(ValueFromPipeline = $true, Mandatory = $true)]
$InputObject,
# Forces the values to be strings and converts them by running them through Out-String
[switch]$AsString,
# If set, empty properties are Included
[switch]$AllowNulls,
# Make each hashtable to have it's own set of properties, otherwise,
# (default) each InputObject is normalized to the properties on the first object in the pipeline
[switch]$DontNormalize
)
BEGIN {
$headers = #()
}
PROCESS {
if (!$headers -or $DontNormalize) {
$headers = $InputObject | Get-Member -type Properties | Select-Object -expand name
}
$OutputHash = #{}
if ($AsString) {
foreach ($col in $headers) {
if ($AllowNulls -or ($InputObject.$col -is [bool] -or ($InputObject.$col))) {
$OutputHash.$col = $InputObject.$col | Out-String -Width 9999 | ForEach-Object { $_.Trim() }
}
}
} else {
foreach ($col in $headers) {
if ($AllowNulls -or ($InputObject.$col -is [bool] -or ($InputObject.$col))) {
$OutputHash.$col = $InputObject.$col
}
}
}
}
END {
return $OutputHash
}
}
Maybe this is overkill but I hope it Helps
Today, the "easiest way" to convert PSCustomObject to Hashtable would be so:
$custom_obj | ConvertTo-HashtableFromPsCustomObject
OR
[hashtable]$custom_obj
Conversely, you can convert a Hashtable to PSCustomObject using:
[PSCustomObject]$hash_table
Only snag is, these nifty options may not be available in older versions of PS