Functions/GenXdev.FileSystem/Find-Item.ps1
################################################################################ <# .SYNOPSIS Performs advanced file and directory searches with content filtering capabilities. .DESCRIPTION A powerful search utility that combines file/directory pattern matching with content filtering. Supports recursive searches, multi-drive operations, and flexible output formats. Can search by name patterns and content patterns simultaneously. .PARAMETER SearchMask File or directory pattern to match against. Supports wildcards (*,?). Default is "*" to match everything. .PARAMETER Pattern Regular expression to search within file contents. Only applies to files. Default is ".*" to match any content. .PARAMETER RelativeBasePath Base directory for generating relative paths in output. Only used when -PassThru is not specified. .PARAMETER AllDrives When specified, searches across all available filesystem drives. .PARAMETER Directory Limits search to directories only, ignoring files. .PARAMETER FilesAndDirectories Includes both files and directories in search results. .PARAMETER PassThru Returns FileInfo/DirectoryInfo objects instead of paths. .PARAMETER IncludeAlternateFileStreams Include alternate data streams in search results. .PARAMETER NoRecurse Prevents recursive searching into subdirectories. .EXAMPLE # Find all files with that have the word "translation" in their content Find-Item -Pattern "translation" # or in short l -mc translation .EXAMPLE # Find any javascript file that tests a version string in it's code Find-Item -SearchMask *.js -Pattern "Version == `"\d\d?\.\d\d?\.\d\d?`"" # or in short l *.js "Version == `"\d\d?\.\d\d?\.\d\d?`"" .EXAMPLE # Find any node_modules\react-dom folder on all drives Find-Item -SearchMask "node_modules\react-dom" -Pattern "Version == `"\d\d?\.\d\d?\.\d\d?`"" # or in short l *.js "Version == `"\d\d?\.\d\d?\.\d\d?`"" .EXAMPLE # Find all directories in the current directory and its subdirectories Find-Item -Directory # or in short l -dir .EXAMPLE # Find all files with the .log extension in all drives Find-Item -SearchMask "*.log" -AllDrives # or in short l *.log -all .EXAMPLE # Find all files with the .config extension and search for the pattern "connectionString" within the files Find-Item -SearchMask "*.config" -Pattern "connectionString" # or in short l *.config connectionString .EXAMPLE # Find all files with the .xml extension and pass the objects through the pipeline Find-Item -SearchMask "*.xml" -PassThru # or in short l *.xml -PassThru .EXAMPLE # Find all files and also include alternate data streams Find-Item -IncludeAlternateFileStreams # or in short l -ads .EXAMPLE # Find only the alternate data streams (not the base files) for all .jpg files Find-Item -SearchMask "*.jpg:" # This syntax automatically enables -IncludeAlternateFileStreams .EXAMPLE # Find jpg files that have a stream named "Zone.Identifier" Find-Item -SearchMask "*.jpg:Zone.Identifier" # No need to specify -IncludeAlternateFileStreams, it's automatically enabled .EXAMPLE # Find all alternate filestreams in the current directory and beyond # containing "secret" text in their content Find-Item -SearchMask "*:*" -Pattern "secret" # This will find all alternate streams in any file that contain the word "secret" .EXAMPLE # Find files with Zone.Identifier streams and return them as objects Find-Item "*:Zone*" -PassThru # Returns System.IO.FileInfo.AlternateDataStream objects with full FileInfo compatibility #> function Find-Item { [CmdletBinding(DefaultParameterSetName = "Default")] [Alias("l")] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseUsingScopeModifierInNewRunspaces", "")] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidTrailingWhitespace", "")] param( ######################################################################## [Parameter( Position = 0, Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, HelpMessage = "File name or pattern to search for. Default is '*'" )] [Alias("like", "l", "Path", "Name", "file", "Query", "FullName")] [ValidateNotNullOrEmpty()] [SupportsWildcards()] [string[]] $SearchMask = "*", ######################################################################## [Parameter( Position = 1, Mandatory = $false, ParameterSetName = 'WithPattern', HelpMessage = "Regular expression pattern to search within content" )] [Alias("mc", "matchcontent")] [ValidateNotNull()] [SupportsWildcards()] [string] $Pattern = ".*", ######################################################################## [Parameter( Position = 2, Mandatory = $false, HelpMessage = "Base path for resolving relative paths in output" )] [Alias("base")] [ValidateNotNullOrEmpty()] [string] $RelativeBasePath = ".\", ######################################################################## [Parameter( Mandatory = $false, HelpMessage = "Search across all available drives" )] [Alias("all")] [switch] $AllDrives, ######################################################################## [Parameter( Mandatory = $false, ParameterSetName = 'DirectoriesOnly', HelpMessage = "Search for directories only" )] [Alias("dir")] [switch] $Directory, ######################################################################## [Parameter( Mandatory = $false, ParameterSetName = 'DirectoriesOnly', HelpMessage = "Include both files and directories" )] [Alias("both")] [switch] $FilesAndDirectories, ######################################################################## [Parameter( Mandatory = $false, HelpMessage = "Output matched items as objects" )] [switch] $PassThru, ######################################################################## [Parameter( Mandatory = $false, HelpMessage = "Include alternate data streams in search results" )] [Alias("ads")] [switch] $IncludeAlternateFileStreams, ######################################################################## [Parameter( Mandatory = $false, HelpMessage = "Do not recurse into subdirectories" )] [switch] $NoRecurse ######################################################################## ) begin { # log function entry with parameters for debugging Microsoft.PowerShell.Utility\Write-Information "BEGIN Find-Item: Initializing with parameters:" Microsoft.PowerShell.Utility\Write-Information " SearchMask: $($SearchMask -join ', ')" Microsoft.PowerShell.Utility\Write-Information " Pattern: $Pattern" Microsoft.PowerShell.Utility\Write-Information " RelativeBasePath: $RelativeBasePath" Microsoft.PowerShell.Utility\Write-Information " AllDrives: $AllDrives" Microsoft.PowerShell.Utility\Write-Information " Directory: $Directory" Microsoft.PowerShell.Utility\Write-Information " FilesAndDirectories: $FilesAndDirectories" Microsoft.PowerShell.Utility\Write-Information " PassThru: $PassThru" Microsoft.PowerShell.Utility\Write-Information " IncludeAlternateFileStreams: $IncludeAlternateFileStreams" Microsoft.PowerShell.Utility\Write-Information " NoRecurse: $NoRecurse" # Track whether we have an implicit ADS request for specific masks $processedSearchMasks = @() $streamPatterns = @{} $searchMasksWithAds = @() # Process SearchMask for alternate data stream patterns foreach ($mask in $SearchMask) { # Skip empty masks if ([string]::IsNullOrWhiteSpace($mask)) { continue } # Expand the mask to a full path to handle drive specifications correctly try { $expandedMask = GenXdev.FileSystem\Expand-Path $mask -ErrorAction SilentlyContinue } catch { # If expansion fails, just use the original mask $expandedMask = $mask Microsoft.PowerShell.Utility\Write-Information "Failed to expand path '$mask': $($_.Exception.Message)" } Microsoft.PowerShell.Utility\Write-Information "Expanded mask '$mask' to '$expandedMask'" # Count the number of colons in the path $colonCount = ($expandedMask -split ':').Length - 1 Microsoft.PowerShell.Utility\Write-Information "Found $colonCount colons in expanded mask" # If we have more than 1 colon (one for drive spec, others for stream) if ($colonCount -gt 1) { $lastColonIndex = $expandedMask.LastIndexOf(':' ) # Extract the file pattern (everything before the last colon) $filePattern = $expandedMask.Substring(0, $lastColonIndex) # Extract the stream pattern (everything after the last colon) $streamPattern = $expandedMask.Substring($lastColonIndex + 1) # If stream pattern is empty, use wildcard to match all streams if ([string]::IsNullOrWhiteSpace($streamPattern)) { $streamPattern = "*" } Microsoft.PowerShell.Utility\Write-Information "Found ADS pattern in SearchMask. File pattern: $filePattern, Stream pattern: $streamPattern" # Store the stream pattern for the expanded file path only $streamPatterns[$filePattern] = $streamPattern # Add only the file pattern to the processed masks and mark it as needing ADS $processedSearchMasks += $filePattern $searchMasksWithAds += $filePattern Microsoft.PowerShell.Utility\Write-Information "Added file pattern to SearchMasksWithAds: $filePattern" } else { # No ADS pattern, add the mask as is $processedSearchMasks += $mask # If IncludeAlternateFileStreams switch is provided, also add this mask to SearchMasksWithAds if ($IncludeAlternateFileStreams) { $searchMasksWithAds += $mask Microsoft.PowerShell.Utility\Write-Information "Added mask to SearchMasksWithAds due to IncludeAlternateFileStreams: $mask" } } } # Replace the original SearchMask with the processed one $SearchMask = $processedSearchMasks # No need to store in script: variables when using $using: in parallel Microsoft.PowerShell.Utility\Write-Information "Modified SearchMask: $($SearchMask -join ', ')" Microsoft.PowerShell.Utility\Write-Information "SearchMasksWithAds: $($searchMasksWithAds -join ', ')" Microsoft.PowerShell.Utility\Write-Information "Stream patterns: $(($streamPatterns | Microsoft.PowerShell.Utility\Out-String))" # user-friendly verbose message about what the function will do Microsoft.PowerShell.Utility\Write-Verbose "Starting search for $($SearchMask -join ', ')$(if(![string]::IsNullOrWhiteSpace($Pattern) -and $Pattern -ne '.*'){" containing text matching pattern: '$Pattern'"})" } process { # log process block entry for debugging Microsoft.PowerShell.Utility\Write-Information "PROCESS Find-Item: Starting search processing" # log high-level search information for users Microsoft.PowerShell.Utility\Write-Verbose "Searching for $(if($Directory){'directories'}elseif($FilesAndDirectories){'files and directories'}else{'files'}) matching $($SearchMask -join ', ')" # if searching across drives, inform the user if ($AllDrives) { Microsoft.PowerShell.Utility\Write-Verbose "Searching across all available drives - this may take some time" Microsoft.PowerShell.Utility\Write-Information "Searching across all available drives" } # parallel search across all filesystem drives if AllDrives switch is provided ($AllDrives ? ( & { # get all filesystem drives with single-letter names Microsoft.PowerShell.Utility\Write-Information "Getting available filesystem drives" $drives = Microsoft.PowerShell.Management\Get-PSDrive -ErrorAction SilentlyContinue | Microsoft.PowerShell.Core\Where-Object { ($PSItem.Provider -Like "*FileSystem") -and ($PSItem.Name.Length -eq 1) } Microsoft.PowerShell.Utility\Write-Verbose "Found drives: $($drives.Name -join ', ')" Microsoft.PowerShell.Utility\Write-Information "Found drives: $($drives.Name -join ', ')" $drives } ) : $null) | Microsoft.PowerShell.Core\ForEach-Object -ThrottleLimit 8 -Parallel { # Access parent scope variables with $using: $streamPatterns = $using:streamPatterns $searchMasksWithAds = $using:searchMasksWithAds $includeAlternateFileStreams = $using:IncludeAlternateFileStreams Microsoft.PowerShell.Utility\Write-Information "Stream patterns in parallel block: $(($streamPatterns | Microsoft.PowerShell.Utility\Out-String))" # helper function to search file contents using regex, including alternate data streams function Search-FileContent { param ( [string] $filePath, [string] $pattern, [string] $streamName = $null ) Microsoft.PowerShell.Utility\Write-Information "Searching file content: $filePath$(if($streamName){":$streamName"}) for pattern: '$pattern'" # Debug information to help identify issues Microsoft.PowerShell.Utility\Write-Information "Pattern type: $($pattern.GetType().FullName), Length: $($pattern.Length)" try { # If a stream name is provided, search in that specific stream content if ($streamName) { # Get the stream content as a string [string] $content = Microsoft.PowerShell.Management\Get-Content -LiteralPath $filePath -Stream $streamName -Raw -ErrorAction Stop if ($content) { # Now apply the regex to the actual content [bool] $matchResult = $content -match $pattern Microsoft.PowerShell.Utility\Write-Information "Stream content match result: $matchResult (explicit regex)" return $matchResult } else { Microsoft.PowerShell.Utility\Write-Information "Stream content is empty" return $false } } # For regular files, use the same regex matching approach for consistency else { # Read the file content directly to use the same matching logic for both [string] $fileContent = Microsoft.PowerShell.Management\Get-Content -LiteralPath $filePath -Raw -ErrorAction Stop if ($fileContent) { [bool] $matchResult = $fileContent -match $pattern Microsoft.PowerShell.Utility\Write-Information "File content match result: $matchResult (explicit regex)" return $matchResult } else { Microsoft.PowerShell.Utility\Write-Information "File is empty" return $false } } } catch { Microsoft.PowerShell.Utility\Write-Information "Error searching content: $($_.Exception.Message)" return $false } } # helper function to recursively search directories function Search-DirectoryContent { param ( [string] $searchPhrase, [bool] $includeAds = $false, [bool] $hasStreamPattern, [string] $expandedSearchMask = $null, [hashtable] $streamPatterns, [bool] $passThru, [string] $relativeBasePath, [string] $pattern, [bool] $directory, [bool] $filesAndDirectories, [bool] $noRecurse ) Microsoft.PowerShell.Utility\Write-Information "Search-DirectoryContent: Starting with phrase: $searchPhrase, includeAds: $includeAds, mask: $expandedSearchMask" # handle empty search phrase by defaulting to current directory if ([string]::IsNullOrWhiteSpace($searchPhrase)) { $searchPhrase = ".\*" Microsoft.PowerShell.Utility\Write-Information "Search phrase was empty, defaulting to: $searchPhrase" } # clean up and normalize the search path $searchPhrase = $searchPhrase.Trim() Microsoft.PowerShell.Utility\Write-Information "Normalized search phrase: $searchPhrase" # ensure proper path termination for directories $endedWithPathSeparator = $searchPhrase.EndsWith( [System.IO.Path]::DirectorySeparatorChar) if ($endedWithPathSeparator) { $searchPhrase += "*" Microsoft.PowerShell.Utility\Write-Information "Path ended with separator, appended wildcard: $searchPhrase" } # convert to absolute path Microsoft.PowerShell.Utility\Write-Information "Converting to absolute path: $searchPhrase" $searchPhrase = GenXdev.FileSystem\Expand-Path $searchPhrase Microsoft.PowerShell.Utility\Write-Information "Absolute path: $searchPhrase" $remainingPath = $searchPhrase # initialize stack for directory traversal Microsoft.PowerShell.Utility\Write-Information "Initializing directory traversal stack" [System.Collections.Generic.Stack[System.Collections.Hashtable]] ` $directories = @() # find the next path separator character $index = $remainingPath.IndexOf([System.IO.Path]::DirectorySeparatorChar) Microsoft.PowerShell.Utility\Write-Information "First path separator index: $index" $indexOriginal = $index # find the first wildcard character (* or ?) $indexWildcard = $remainingPath.IndexOf("*") $indexQuestionMark = $remainingPath.IndexOf("?") Microsoft.PowerShell.Utility\Write-Information "First wildcard positions - * at: $indexWildcard, ? at: $indexQuestionMark" # if question mark comes before asterisk, use that as the wildcard position if ($indexQuestionMark -ge 0 -and ($indexWildcard -lt 0 -or $indexQuestionMark -lt $indexWildcard)) { $indexWildcard = $indexQuestionMark Microsoft.PowerShell.Utility\Write-Information "Using ? as first wildcard at position: $indexWildcard" } # determine if we're at the last path component $last = $index -eq -1 Microsoft.PowerShell.Utility\Write-Information "Is last path component: $last" # be more efficient by skipping directories that don't require a match # have no wildcard or a wildcard in the path that comes after next directory separator? if ((-not $last) -and (($indexWildCard -lt 0) -or ($indexWildcard -gt $index))) { Microsoft.PowerShell.Utility\Write-Information "Optimizing directory traversal path" # determine start position for searching wildcard preceding directory separator $index = $indexWildcard -lt 0 ? $index : $indexWildcard Microsoft.PowerShell.Utility\Write-Information "Adjusted index: $index" # determine if there is a wildcard after the wildcard character $index2 = $indexWildcard -lt 0 ? -1 : $remainingPath.IndexOf([System.IO.Path]::DirectorySeparatorChar, $indexWildcard) Microsoft.PowerShell.Utility\Write-Information "Wildcard delimiter index: $index2" # if wildcard was found, search for the preceding directory separator character if ($indexWildCard -ge 0) { Microsoft.PowerShell.Utility\Write-Information "Finding directory separator before wildcard" while ($index -ge 1 -and ($remainingPath[$index] -ne [System.IO.Path]::DirectorySeparatorChar)) { $index-- } Microsoft.PowerShell.Utility\Write-Information "Found at index: $index" } # wildcard was found and did not have a preceding directory separator character? if ($index2 -lt 0) { # set last flag to true, for later processing $last = $true Microsoft.PowerShell.Utility\Write-Information "Setting last flag to true as wildcard has no following separator" # if wildcard was present, adjust position to exclude directory with wildcard if ($indexWildcard -ge 0) { # move cursor to one character before directory separator $index--; Microsoft.PowerShell.Utility\Write-Information "Adjusted index position: $index" } else { # if no wildcard was found, move cursor to the last directory separator $index = $remainingPath.LastIndexOf( [System.IO.Path]::DirectorySeparatorChar) - 1; Microsoft.PowerShell.Utility\Write-Information "No wildcard found, moved to last separator: $index" } # exclude the directory holding the wildcard from our next directory scan Microsoft.PowerShell.Utility\Write-Information "Excluding directory with wildcard from scan" while ($index -ge 1 -and ($remainingPath[$index] -ne [System.IO.Path]::DirectorySeparatorChar)) { $index-- } Microsoft.PowerShell.Utility\Write-Information "Final index position: $index" } } # prepare the search path for the first directory scan $searchPath = "$($currentPath)*" Microsoft.PowerShell.Utility\Write-Information "Initial search path: $searchPath" # have no wildcard or a wildcard in the path that comes after next directory separator? if (($index -ge 0) -and (($indexWildcard -lt 0) -or ($indexWildcard -gt $index))) { Microsoft.PowerShell.Utility\Write-Information "Adjusting search path based on wildcard position" # wildcard was found and did not have a preceding directory separator character? if ($last) { # find the last directory separator character $i = $remainingPath.LastIndexOf([System.IO.Path]::DirectorySeparatorChar) Microsoft.PowerShell.Utility\Write-Information "Last directory separator at: $i" # set the appropriate path to search $searchPath = "$($currentPath)$($remainingPath.Substring(0, $i))\*" Microsoft.PowerShell.Utility\Write-Information "Updated search path: $searchPath" } else { # set the appropriate path to search $searchPath = "$($currentPath)$($remainingPath.Substring(0, $indexWildcard))*" Microsoft.PowerShell.Utility\Write-Information "Updated search path with wildcard: $searchPath" } } # push the first directory scan onto the stack Microsoft.PowerShell.Utility\Write-Information "Pushing first directory scan to stack" $null = $directories.Push( @{ currentPath = $remainingPath.Substring(0, $index + 1) remainingPath = $remainingPath.Substring($index + 1) currentDepth = 0 } ) Microsoft.PowerShell.Utility\Write-Information "Stack entry - currentPath: $($directories.Peek().currentPath), remainingPath: $($directories.Peek().remainingPath)" # process directories using a stack for efficient traversal [hashtable]$folder = $null Microsoft.PowerShell.Utility\Write-Information "Starting directory stack processing loop" $stackProcessCount = 0 $totalDirsProcessed = 0 $totalFilesChecked = 0 $totalMatches = 0 while ($directories.TryPop([ref]$folder)) { $stackProcessCount++ $totalDirsProcessed++ # every 10 directories, update the verbose message for users if ($totalDirsProcessed % 10 -eq 0) { Microsoft.PowerShell.Utility\Write-Verbose "Searched $totalDirsProcessed directories, found $totalMatches matches so far..." } Microsoft.PowerShell.Utility\Write-Information "Processing stack item #$stackProcessCount - currentPath: $($folder.currentPath), remainingPath: $($folder.remainingPath), depth: $($folder.currentDepth)" # find the next directory separator in the remaining path $index = $folder.remainingPath.IndexOf([System.IO.Path]::DirectorySeparatorChar) Microsoft.PowerShell.Utility\Write-Information "Next directory separator in remaining path: $index" # save the original index for later use $indexOriginal = $index # find the first wildcard in the remaining path $indexWildcard = $folder.remainingPath.IndexOf("*") $indexQuestionMark = $folder.remainingPath.IndexOf("?") Microsoft.PowerShell.Utility\Write-Information "Wildcards in remaining path - * at: $indexWildcard, ? at: $indexQuestionMark" # if question mark comes before asterisk, use that as the wildcard position if ($indexQuestionMark -ge 0 -and ($indexWildcard -lt 0 -or $indexQuestionMark -lt $indexWildcard)) { $indexWildcard = $indexQuestionMark Microsoft.PowerShell.Utility\Write-Information "Using ? as wildcard position: $indexWildcard" } # determine if this is the last directory in the path $last = $index -eq -1 Microsoft.PowerShell.Utility\Write-Information "Is last directory component: $last" # be more efficient by skipping directories that don't require a match # have no wildcard or a wildcard in the path that comes after next directory separator? if ((-not $last) -and (($indexWildCard -lt 0) -or ($indexWildcard -gt $index))) { Microsoft.PowerShell.Utility\Write-Information "Optimizing intermediate directory traversal" # determine start position for searching wildcard preceding directory separator $index = $indexWildcard -lt 0 ? $index - 1 : $indexWildcard Microsoft.PowerShell.Utility\Write-Information "Adjusted intermediate index: $index" # determine if there is a wildcard after the wildcard character $index2 = $indexWildcard -lt 0 ? -1 : $folder.remainingPath.IndexOf( [System.IO.Path]::DirectorySeparatorChar, $indexWildcard); Microsoft.PowerShell.Utility\Write-Information "Intermediate wildcard delimiter index: $index2" # if wildcard was found, search for the preceding directory separator character if ($indexWildCard -ge 0) { Microsoft.PowerShell.Utility\Write-Information "Finding directory separator before intermediate wildcard" while ($index -ge 1 -and ($folder.remainingPath[$index] -ne [System.IO.Path]::DirectorySeparatorChar)) { $index-- } Microsoft.PowerShell.Utility\Write-Information "Found at intermediate index: $index" } # wildcard was found and did not have a preceding directory separator character? if ($index2 -lt 0) { Microsoft.PowerShell.Utility\Write-Information "Intermediate wildcard has no following separator" # set last flag to true, for later processing $last = $true # if wildcard was present, adjust position to exclude directory with wildcard if ($indexWildcard -ge 0) { # move cursor to one character before directory separator $index--; Microsoft.PowerShell.Utility\Write-Information "Adjusted intermediate index position: $index" } else { # if no wildcard was found, move cursor to the last directory separator $index = $remainingPath.LastIndexOf( [System.IO.Path]::DirectorySeparatorChar) - 1; Microsoft.PowerShell.Utility\Write-Information "No intermediate wildcard found, moved to last separator: $index" } # exclude directory holding the wildcard from our next directory scan Microsoft.PowerShell.Utility\Write-Information "Excluding intermediate directory with wildcard from scan" while ($index -ge 1 -and ($folder.remainingPath[$index] -ne [System.IO.Path]::DirectorySeparatorChar)) { $index-- } Microsoft.PowerShell.Utility\Write-Information "Final intermediate index position: $index" } } # prepare the search path for the next directory scan $searchPath = "$($folder.currentPath)*" Microsoft.PowerShell.Utility\Write-Information "Next search path: $searchPath" # have no wildcard or a wildcard in the path that comes after next directory separator? if (($index -ge 0) -and (($indexWildcard -lt 0) -or ($indexWildcard -gt $index))) { Microsoft.PowerShell.Utility\Write-Information "Adjusting next search path based on wildcard position" # wildcard was found and did not have a preceding directory separator character? if ($last) { Microsoft.PowerShell.Utility\Write-Information "Last directory with wildcard handling" # find the last directory separator character $i = $folder.remainingPath.LastIndexOf( [System.IO.Path]::DirectorySeparatorChar) Microsoft.PowerShell.Utility\Write-Information "Last directory separator at: $i" # set the appropriate path to search $searchPath = "$($folder.currentPath)$($folder.remainingPath.Substring(0, $i))\*" Microsoft.PowerShell.Utility\Write-Information "Updated final search path: $searchPath" # set the name to match for the next directory scan $nameToMatch = $folder.remainingPath.Substring($i + 1) Microsoft.PowerShell.Utility\Write-Information "Name pattern to match: $nameToMatch" } else { Microsoft.PowerShell.Utility\Write-Information "Intermediate directory with wildcard handling" # set the appropriate path to search $searchPath = "$($folder.currentPath)$($folder.remainingPath.Substring(0, $indexWildcard))*" Microsoft.PowerShell.Utility\Write-Information "Updated intermediate search path: $searchPath" # set the name to match for the next directory scan $nameToMatch = $folder.remainingPath Microsoft.PowerShell.Utility\Write-Information "Intermediate name pattern to match: $nameToMatch" } } else { # are we following a /**/ pattern but haven't found the first matching directory yet? if ($folder.forwardSearch) { Microsoft.PowerShell.Utility\Write-Information "Following /**/ recursive pattern search" # set the name to match for the next directory scan $nameToMatch = $folder.nameToMatch Microsoft.PowerShell.Utility\Write-Information "Recursive pattern name to match: $nameToMatch" # force the last flag to true to keep following /**/ pattern without # losing information about directory to match next $last = $folder.remainingPath.Substring(3).IndexOf( [System.IO.Path]::DirectorySeparatorChar) -lt 0; Microsoft.PowerShell.Utility\Write-Information "Updated last flag for recursive pattern: $last" } else { Microsoft.PowerShell.Utility\Write-Information "Standard pattern matching" # set the name to match for the next directory scan $nameToMatch = $folder.remainingPath Microsoft.PowerShell.Utility\Write-Information "Standard name pattern to match: $nameToMatch" # log that we've reached the end of the path pattern Microsoft.PowerShell.Utility\Write-Information ( "No more directories to match in " + "$($folder.currentPath) - setting last flag to true") } } # if we are not at the last directory in the path if (-not $last) { Microsoft.PowerShell.Utility\Write-Information "Not at last directory, processing intermediate directories" # and we are not following a /**/ pattern if (-not $folder.forwardSearch) { Microsoft.PowerShell.Utility\Write-Information "Not in /**/ pattern search mode" # set next directory to match to be the next directory in the path $nameToMatch = $folder.remainingPath.Substring(0, $indexOriginal) Microsoft.PowerShell.Utility\Write-Information "Directory name to match: $nameToMatch" } # get only directories since there are more directories to match $directorySearchOption = [System.IO.SearchOption]::TopDirectoryOnly Microsoft.PowerShell.Utility\Write-Information "Getting directories from: $searchPath" try { # use System.IO.Directory to get directories instead of Get-ChildItem $searchDir = [System.IO.Path]::GetDirectoryName($searchPath) $searchPattern = [System.IO.Path]::GetFileName($searchPath) Microsoft.PowerShell.Utility\Write-Information "Search directory: $searchDir, pattern: $searchPattern" $directories_to_process = [System.IO.Directory]::GetDirectories( $searchDir, $searchPattern, $directorySearchOption) Microsoft.PowerShell.Utility\Write-Information "Found $(if($directories_to_process){$directories_to_process.Count}else{0}) directories to process" foreach ($dirPath in $directories_to_process) { # create DirectoryInfo object to match PowerShell behavior $dirInfo = Microsoft.PowerShell.Utility\New-Object System.IO.DirectoryInfo($dirPath) Microsoft.PowerShell.Utility\Write-Information "Processing directory: $($dirInfo.FullName)" # are we following a /**/ pattern? if ($folder.forwardSearch) { Microsoft.PowerShell.Utility\Write-Information "In /**/ search mode, checking if directory matches pattern: $($folder.nameToMatch)" # is this the next directory to match if ($dirInfo.Name -like $nameToMatch) { Microsoft.PowerShell.Utility\Write-Information "Found matching directory for /**/ pattern: $($dirInfo.Name)" $remainingPath = $folder.remainingPath.Substring(3); $i = $remainingPath.IndexOf([System.IO.Path]::DirectorySeparatorChar) if ($i -ge 0) { $remainingPath = $remainingPath.Substring($i + 1) } Microsoft.PowerShell.Utility\Write-Information "Remaining path after match: $remainingPath" # schedule directory scan that stops following the /**/ pattern $null = $directories.Push( @{ remainingPath = $remainingPath currentPath = "$($folder.currentPath)$($dirInfo.Name)\" currentDepth = $folder.currentDepth + 1 } ) Microsoft.PowerShell.Utility\Write-Information ( "Ending /**/ search for $nameToMatch in " + "$($directories.Peek().currentPath)") } else { Microsoft.PowerShell.Utility\Write-Information "Directory doesn't match /**/ pattern, continuing search" # schedule directory scan that will continue following /**/ pattern $null = $directories.Push( @{ forwardSearch = $true remainingPath = $folder.remainingPath currentPath = "$($folder.currentPath)$($dirInfo.Name)\" currentDepth = $folder.currentDepth + 1 nameToMatch = $folder.remainingPath.Substring(3).Split( [System.IO.Path]::DirectorySeparatorChar)[0] } ) Microsoft.PowerShell.Utility\Write-Information ( "Continuing following /**/ search for " + "$($directories.Peek().$nameToMatch) in " + "$($directories.Peek().currentPath)") } } # check if we are entering a /**/ pattern elseif ($nameToMatch -eq "**") { Microsoft.PowerShell.Utility\Write-Information "Entering /**/ recursive search pattern" # schedule directory scan that will start following the /**/ pattern $null = $directories.Push( @{ forwardSearch = $true remainingPath = $folder.remainingPath currentPath = "$($folder.currentPath)$($dirInfo.Name)\" currentDepth = $folder.currentDepth + 1 nameToMatch = $folder.remainingPath.Substring(3).Split( [System.IO.Path]::DirectorySeparatorChar)[0] } ) Microsoft.PowerShell.Utility\Write-Information ( "Starting /**/ search for " + "$($directories.Peek().$nameToMatch) in " + "$($directories.Peek().currentPath)") } # only schedule directories that match the name to match elseif ($dirInfo.Name -like $nameToMatch) { Microsoft.PowerShell.Utility\Write-Information "Directory name '$($dirInfo.Name)' matches pattern '$nameToMatch'" # push directory onto stack for processing $null = $directories.Push( @{ remainingPath = $folder.remainingPath.Substring($index + 1) currentPath = "$($folder.currentPath)$($dirInfo.Name)\" currentDepth = $folder.currentDepth + 1 } ) Microsoft.PowerShell.Utility\Write-Information ( "Matched next directory for $nameToMatch in " + "$($directories.Peek().currentPath)") } else { Microsoft.PowerShell.Utility\Write-Information "Directory name '$($dirInfo.Name)' does not match pattern '$nameToMatch'" } } } catch { # log any errors accessing directories Microsoft.PowerShell.Utility\Write-Information ( "Error accessing directory: $([System.IO.Path]::GetDirectoryName($searchPath)) - $($_.Exception.Message)") } # skip to next directory in the stack Microsoft.PowerShell.Utility\Write-Information "Continuing to next directory in stack" continue; } # we are at the last directory of the SearchPhrase supplied Microsoft.PowerShell.Utility\Write-Information "Reached last directory component, performing final matching" # get both files and directories for final matching try { $searchOption = [System.IO.SearchOption]::TopDirectoryOnly $searchDir = [System.IO.Path]::GetDirectoryName($searchPath) $searchPattern = [System.IO.Path]::GetFileName($searchPath) Microsoft.PowerShell.Utility\Write-Information "Final search in directory: $searchDir with pattern: $searchPattern" Microsoft.PowerShell.Utility\Write-Verbose "Searching in directory: $searchDir" # get directories if requested Microsoft.PowerShell.Utility\Write-Information "Directory search enabled: $($directory -or $filesAndDirectories -or (-not $directory))" $directories_found = @() if ($directory -or $filesAndDirectories -or (-not $directory)) { Microsoft.PowerShell.Utility\Write-Information "Searching for directories matching pattern" $directories_found = [System.IO.Directory]::GetDirectories( $searchDir, $searchPattern, $searchOption) Microsoft.PowerShell.Utility\Write-Information "Found $(if($directories_found){$directories_found.Count}else{0}) matching directories" } # get files if not directories only $files_found = @() if (-not $directory) { Microsoft.PowerShell.Utility\Write-Information "Searching for files matching pattern" $files_found = [System.IO.Directory]::GetFiles( $searchDir, $searchPattern, $searchOption) $totalFilesChecked += $files_found.Count Microsoft.PowerShell.Utility\Write-Information "Found $(if($files_found){$files_found.Count}else{0}) matching files" } # combine results $all_items = $directories_found + $files_found Microsoft.PowerShell.Utility\Write-Information "Total items found in this directory: $(if($all_items){$all_items.Count}else{0})" foreach ($itemPath in $all_items) { # create appropriate info object based on item type $isDirectory = [System.IO.Directory]::Exists($itemPath) $itemInfo = $isDirectory ? (Microsoft.PowerShell.Utility\New-Object System.IO.DirectoryInfo($itemPath)) : (Microsoft.PowerShell.Utility\New-Object System.IO.FileInfo($itemPath)) Microsoft.PowerShell.Utility\Write-Information "Processing $(if($isDirectory){'directory'}else{'file'}): $($itemInfo.FullName)" # if we find directories, recurse if not disabled if ($isDirectory -and (-not $noRecurse)) { Microsoft.PowerShell.Utility\Write-Information "Will recurse into directory: $($itemInfo.FullName)" # schedule directory scan for this additionally found directory $null = $directories.Push( @{ remainingPath = ($Last) ? "$nameToMatch" : "*" currentPath = "$($itemInfo.FullName)\" currentDepth = $folder.currentDepth + 1 } ) Microsoft.PowerShell.Utility\Write-Information ( "Recursing after last matched directory in " + "$($directories.Peek().currentPath)") } # if item doesn't match name pattern, skip it if (-not ($itemInfo.Name -like $nameToMatch)) { Microsoft.PowerShell.Utility\Write-Information "Skipping item: '$($itemInfo.FullName)' - doesn't match name pattern: '$nameToMatch'" continue } Microsoft.PowerShell.Utility\Write-Information "Item name '$($itemInfo.Name)' matches pattern '$nameToMatch'" # check if item type matches what user wants $typeOk = ($isDirectory -and ($directory -or $filesAndDirectories)) -or ((-not $directory) -and (-not $isDirectory)) if (-not $typeOk) { Microsoft.PowerShell.Utility\Write-Information "Skipping item: item type doesn't match requested type" continue } Microsoft.PowerShell.Utility\Write-Information "Item type matches filter criteria" $hasStreamPattern = $null -ne $streamPatterns[$expandedSearchMask] # if this is a file, check content pattern if specified $contentMatch = $isDirectory -or ($hasStreamPattern) -or [string]::IsNullOrWhiteSpace($pattern) -or ($pattern -eq ".*") if (-not $contentMatch) { Microsoft.PowerShell.Utility\Write-Information "Checking file content against pattern: $pattern" # Fix: Use the actual boolean result instead of checking if non-null $contentMatch = Search-FileContent -FilePath ($itemInfo.FullName) -Pattern $pattern Microsoft.PowerShell.Utility\Write-Information "Content match result: $contentMatch" } if ($contentMatch) { $totalMatches++ Microsoft.PowerShell.Utility\Write-Information "Found matching item: $($itemInfo.FullName)" Microsoft.PowerShell.Utility\Write-Verbose "Found match: $($itemInfo.FullName)" # Determine if we should output the base file - simplified check # Only check if the expanded path has a stream pattern $shouldOutputFile = -not $hasStreamPattern if ($shouldOutputFile) { # output FileInfo/DirectoryInfo objects if -PassThru is specified if ($passThru) { Microsoft.PowerShell.Utility\Write-Information "Returning object directly (PassThru mode)" Microsoft.PowerShell.Utility\Write-Output $itemInfo } else { # output relative path of the found item Microsoft.PowerShell.Utility\Write-Information "Resolving relative path with base: $relativeBasePath" $rp = Microsoft.PowerShell.Management\Resolve-Path -LiteralPath $itemInfo.FullName ` -Relative -RelativeBasePath:$relativeBasePath Microsoft.PowerShell.Utility\Write-Information "Relative path: $rp" Microsoft.PowerShell.Utility\Write-Output $rp } } else { Microsoft.PowerShell.Utility\Write-Information "Skipping base file because a stream pattern was specified for this search mask" } } else { Microsoft.PowerShell.Utility\Write-Information "Item content doesn't match pattern, skipping" } # If IncludeAlternateFileStreams is specified and this is a file if ((-not $isDirectory) -and $includeAds) { Microsoft.PowerShell.Utility\Write-Information "Getting alternate data streams for file: $($itemInfo.FullName)" try { # Get all streams for this file $streams = Microsoft.PowerShell.Management\Get-Item -LiteralPath $itemInfo.FullName -Stream * -ErrorAction SilentlyContinue | Microsoft.PowerShell.Core\Where-Object { ($_.Stream -ne ':$DATA') -or $hasStreamPattern } # Skip the default stream Microsoft.PowerShell.Utility\Write-Information "Found $(if($streams){$streams.Count}else{0}) alternate data streams" # Check if we have specific stream patterns to match against $streamPatternsForFile = $streamPatterns."$expandedSearchMask" if ([string]::IsNullOrWhiteSpace($streamPatternsForFile)) { $streamPatternsForFile = "*" } foreach ($stream in $streams) { Microsoft.PowerShell.Utility\Write-Information "Processing stream: $($stream.Stream) of size: $($stream.Length)" # If we have specific stream patterns, check if this stream matches $streamNameMatch = $true if ($streamPatternsForFile) { $streamNameMatch = $stream.Stream -like $streamPatternsForFile Microsoft.PowerShell.Utility\Write-Information "Checking stream name '$($stream.Stream)' against pattern '$streamPatternsForFile': $streamNameMatch" } # Skip this stream if it doesn't match the stream pattern if (-not $streamNameMatch) { Microsoft.PowerShell.Utility\Write-Information "Stream name doesn't match pattern, skipping" continue } # Check if Pattern parameter is specified and not the default value $streamContentMatch = [string]::IsNullOrWhiteSpace($pattern) -or ($pattern -eq ".*") if (-not $streamContentMatch) { # Search for pattern in stream content Microsoft.PowerShell.Utility\Write-Information "Checking stream content against pattern: $pattern" # Fix: Use the actual boolean result instead of checking if non-null $streamContentMatch = Search-FileContent -FilePath $itemInfo.FullName -Pattern $pattern -StreamName $stream.Stream Microsoft.PowerShell.Utility\Write-Information "Stream content match result: $streamContentMatch" } # Only process streams that match the pattern (if specified) if ($streamContentMatch) { if ($passThru) { # Create a PSCustomObject clone of the FileInfo with stream info # This maintains compatibility with FileInfo while including stream information $properties = @{} # Copy all properties from the original FileInfo object foreach ($property in $itemInfo.PSObject.Properties) { if ($property.Name -eq 'FullName') { $properties['FullName'] = "$($itemInfo.FullName):$($stream.Stream)" } elseif ($property.Name -eq 'Name') { $properties['Name'] = "$($itemInfo.Name):$($stream.Stream)" } else { $properties[$property.Name] = $property.Value } } # Add additional stream properties $properties['Stream'] = $stream.Stream $properties['StreamLength'] = $stream.Length # Create the custom object with the properties $streamObj = [PSCustomObject]$properties # Add a type name to help with type conversion $streamObj.PSObject.TypeNames.Insert(0, "System.IO.FileInfo.AlternateDataStream") $streamObj.PSObject.TypeNames.Insert(1, "System.IO.FileInfo") Microsoft.PowerShell.Utility\Write-Output $streamObj } else { # For path output format the path with stream $rp = Microsoft.PowerShell.Management\Resolve-Path -LiteralPath $itemInfo.FullName ` -Relative -RelativeBasePath:$relativeBasePath Microsoft.PowerShell.Utility\Write-Output "$rp`:$($stream.Stream)" } } else { Microsoft.PowerShell.Utility\Write-Information "Stream content doesn't match pattern, skipping" } } } catch { Microsoft.PowerShell.Utility\Write-Information "Error accessing alternate data streams: $($_.Exception.Message)" } } } } catch { # log any errors processing directories Microsoft.PowerShell.Utility\Write-Information ( "Error processing directory: $searchDir - $($_.Exception.Message)") Microsoft.PowerShell.Utility\Write-Verbose "Error accessing $searchDir - $($_.Exception.Message)" } } Microsoft.PowerShell.Utility\Write-Information "Directory stack processing complete - processed $stackProcessCount items" Microsoft.PowerShell.Utility\Write-Verbose "Search complete: Examined $totalDirsProcessed directories and $totalFilesChecked files, found $totalMatches matches" } # process each search mask provided foreach ($currentSearchPhrase in $using:SearchMask) { Microsoft.PowerShell.Utility\Write-Information "Processing search pattern: $currentSearchPhrase" Microsoft.PowerShell.Utility\Write-Verbose "Processing search pattern: $currentSearchPhrase" $expandedSearchMask = GenXdev.FileSystem\Expand-Path $currentSearchPhrase # Check if this specific search mask should include alternate data streams # Simplified to only check the expanded search mask $hasStreamPattern = $null -ne $streamPatterns[$expandedSearchMask] $includeAds = $includeAlternateFileStreams -or $hasStreamPattern if ($includeAds) { Microsoft.PowerShell.Utility\Write-Information "This search mask should include ADS: $currentSearchPhrase" } # if not a multi-drive search or currently processing root context if ($null -eq $PSItem) { Microsoft.PowerShell.Utility\Write-Information "Searching in current context (not drive-specific)" Search-DirectoryContent -SearchPhrase $currentSearchPhrase ` -IncludeAds $includeAds ` -HasStreamPattern $hasStreamPattern ` -ExpandedSearchMask $expandedSearchMask ` -StreamPatterns $streamPatterns ` -PassThru $using:PassThru ` -RelativeBasePath $using:RelativeBasePath ` -Pattern $using:Pattern ` -Directory $using:Directory ` -FilesAndDirectories $using:FilesAndDirectories ` -NoRecurse $using:NoRecurse } else { $expandedSearchMask = GenXdev.FileSystem\Expand-Path $currentSearchPhrase ` -ForceDrive $PSItem.Name # force the search to start from the specific drive Microsoft.PowerShell.Utility\Write-Information "Searching on drive $($PSItem.Name)" Microsoft.PowerShell.Utility\Write-Verbose "Searching on drive $($PSItem.Name)" Microsoft.PowerShell.Utility\Write-Information "Expanded path for drive $($PSItem.Name): $expandedSearchMask" Search-DirectoryContent -SearchPhrase $expandedSearchMask ` -IncludeAds $includeAds ` -HasStreamPattern $hasStreamPattern ` -ExpandedSearchMask $expandedSearchMask ` -StreamPatterns $streamPatterns ` -PassThru $using:PassThru ` -RelativeBasePath $using:RelativeBasePath ` -Pattern $using:Pattern ` -Directory $using:Directory ` -FilesAndDirectories $using:FilesAndDirectories ` -NoRecurse $using:NoRecurse } } } Microsoft.PowerShell.Utility\Write-Information "PROCESS Find-Item: Search processing completed" Microsoft.PowerShell.Utility\Write-Verbose "Search completed" } end { Microsoft.PowerShell.Utility\Write-Information "END Find-Item: Function execution completed" } } ################################################################################ |