Public/Find-Item.ps1
function Find-Item { <# .SYNOPSIS Simple and fast function for finding any item on the filesystem (like find on Linux/Unix) .DESCRIPTION Function that uses the EnumerateFiles, EnumerateDirectories, EnumerateFileSystemEntries method from the dotnet class System.Io.Directory (and System.IO.DirectoryInfo for typed output) to quickly find any item on the filesystem. Item could be a directory or a file or anything else. Class System.IO.EnumerationOptions does not exist in PowerShell < 6 (so this function is not supported in the normal Windows PowerShell, only in PowerShell Core/7) .PARAMETER Path Root path to search items for. Defaults to current working directory. The relative or absolute path to the directory to search. This string is not case-sensitive. Wildcards ARE allowed (e.g., 'C:\Users\*\Documents') and will expand to MULTIPLE root paths (Linux-like behavior). .PARAMETER Name (Default: '*') This is the searchPattern for the Enumeration class. The search string to match against the names of items in path. This parameter can contain a combination of valid literal and wildcard characters, but it doesn't support regular expressions. You can use the * (asterisk) to match zero or more characters in that position. You can also use the ? (question mark) to exactly match one character in that position. Default is '*' = all items One ore more strings to search for (f.e. '*.exe' OR '*.exe','*.log' OR 'foo*.log'). NOTE: Platform casing behavior - Windows: PlatformDefault is case-insensitive (=> -Name behaves like Linux -iname). - Linux/macOS: PlatformDefault is case-sensitive (=> -Name behaves like Linux -name). .PARAMETER Iname Case-insensitive variant of -Name (like Linux find -iname). If provided, it sets MatchCasing to CaseInsensitive (unless MatchCasing was explicitly specified) and uses these patterns in addition to or instead of -Name. On Windows, -Name and -Iname behave the same (both case-insensitive). On Linux/macOS they differ. .PARAMETER Type Only search items of specific type: Directory, File or All .PARAMETER Recurse EnumerationOptions property RecurseSubdirectories. Check https://learn.microsoft.com/dotnet/api/system.io.enumerationoptions for more information. .PARAMETER IgnoreInaccessible EnumerationOptions property IgnoreInaccessible. Check https://learn.microsoft.com/dotnet/api/system.io.enumerationoptions for more information. .PARAMETER As Could be 'String', 'FileSystemInfo' or 'FileInfo'. OutputType of found items will be: - String : array of full paths (fastest) - FileSystemInfo : native typed objects (FileInfo/DirectoryInfo) using DirectoryInfo.Enumerate* - FileInfo : backward compatibility; for files returns FileInfo, for directories returns DirectoryInfo (no breaking change) .PARAMETER MatchCasing EnumerationOptions property MatchCasing. Check https://learn.microsoft.com/dotnet/api/system.io.enumerationoptions for more information. .PARAMETER AttributesToSkip EnumerationOptions property AttributesToSkip. Check https://learn.microsoft.com/dotnet/api/system.io.enumerationoptions for more information. Defaults to FileAttributes.Hidden | FileAttributes.System. Specify 0 to disable. .PARAMETER MatchType EnumerationOptions property MatchType. Check https://learn.microsoft.com/dotnet/api/system.io.enumerationoptions for more information. .PARAMETER Depth EnumerationOptions property MaxRecursionDepth. Implies RecurseSubdirectories = $true when provided. .PARAMETER MinDepth Minimum directory depth relative to Path before items are returned (like Linux -mindepth). Default 0. .PARAMETER IncludeSpecialDirectories EnumerationOptions property ReturnSpecialDirectories. Check https://learn.microsoft.com/dotnet/api/system.io.enumerationoptions for more information. .EXAMPLE PS C:\> Find-Item -Path c:\windows -Name '*.exe' -As FileSystemInfo Find all items with file format exe in c:\windows without subdirectory and return each file as FileSystemInfo object .EXAMPLE PS C:\> psfind uses alias psfind for Find-Item. returns all items (files + directories) with full path in current folder .EXAMPLE PS C:\> search uses alias search for Find-Item. returns all items (files + directories) with full path in current folder .EXAMPLE PS C:\> psfind / -Iname '*.exe' -Recurse Linux-like: find / -iname '*.exe' (case-insensitive on all platforms). .EXAMPLE PS C:\> psfind -Path 'C:\Users\*\Documents' -Type File -Name '*.log' -Recurse Linux-like wildcard expansion of multiple root paths: searches every matching Users\<name>\Documents. .LINK https://github.com/eizedev/PSItems .LINK https://learn.microsoft.com/dotnet/api/system.io.directoryinfo .LINK https://learn.microsoft.com/dotnet/api/system.io.enumerationoptions .NOTES Author: Eizedev Last Modified: Sep 5, 2025 Version: 1.7 #> #Requires -PSEdition Core [CmdletBinding()] [OutputType('System.String', 'System.IO.FileSystemInfo')] param ( # Path to search for files. Defaults to current directory. [Parameter(Mandatory = $false, Position = 0)] [string] $Path = $pwd, # Name for searching for files [Parameter(Mandatory = $false, Position = 1)] [string[]] $Name = '*', # Case-insensitive name(s), like Linux find -iname. Merges with or replaces -Name. [Parameter(Mandatory = $false)] [string[]] $Iname, # Type if the items [Parameter(Mandatory = $false)] [ValidateSet('Directory', 'File', 'All')] [string] $Type = 'All', # Include subdirectories if given [Parameter(Mandatory = $false)] [switch] $Recurse, # Sets a value that indicates whether to skip files or directories when access is denied (for example, UnauthorizedAccessException or SecurityException). Default is true [Parameter(Mandatory = $false)] [bool] $IgnoreInaccessible = $true, # Convert given file path to FileSystemInfo or String. 'FileInfo' kelspt for backward compatibility. [Parameter(Mandatory = $false)] [ValidateSet('String', 'FileSystemInfo', 'FileInfo')] [string] $As = 'String', # Match case if given [Parameter(Mandatory = $false)] [System.IO.MatchCasing] [ValidateSet('PlatformDefault', 'CaseSensitive', 'CaseInsensitive')] $MatchCasing = [System.IO.MatchCasing]::PlatformDefault, # Attributes of files to skip (not to search for) (Defaults to FileAttributes.Hidden | FileAttributes.System). Specify 0 to disable [Parameter(Mandatory = $false)] [ValidateSet(0, 'ReadOnly', 'Hidden', 'System', 'Directory', 'Archive', 'Device', 'Normal', 'Temporary', 'SparseFile', 'ReparsePoint', 'Compressed', 'Offline', 'NotContentIndexed', 'Encrypted', 'IntegrityStream', 'NoScrubData')] [object[]] $AttributesToSkip = @('Hidden', 'System'), # sets the match type [Parameter(Mandatory = $false)] [System.IO.MatchType] [ValidateSet('Simple', 'Win32')] $MatchType = [System.IO.MatchType]::Simple, # sets a value that indicates the maximum directory depth to recurse while enumerating (RecurseSubdirectories (-Recurse) must be to true) [Parameter(Mandatory = $false)] [int32] $Depth, # Minimum directory depth to return (like Linux -mindepth). 0 = include items at root level. [Parameter(Mandatory = $false)] [int32] $MinDepth = 0, # if given, return the special directory entries "." and ".."; otherwise, false [Parameter(Mandatory = $false)] [switch] $IncludeSpecialDirectories ) # Outputs all function parameters of type string[], bool, switch and int32 (including defaults) as verbose messages. if ($PSBoundParameters.ContainsKey('Verbose')) { $PSCmdlet.MyInvocation.MyCommand.Parameters.Keys | ForEach-Object { $val = Get-Variable -Name $_ -ValueOnly -ErrorAction SilentlyContinue if ($null -ne $val) { if ($val -is [Array]) { Write-Verbose "$($_): '$($val -join "', '")'" } else { Write-Verbose "$($_): '$val'" } } } } # If -Iname is used, merge/override patterns and default to case-insensitive if user didn't set MatchCasing if ($PSBoundParameters.ContainsKey('Iname')) { if (-not $PSBoundParameters.ContainsKey('Name') -or ($Name -eq '*')) { $Name = $Iname } else { $Name += $Iname } if (-not $PSBoundParameters.ContainsKey('MatchCasing')) { $MatchCasing = [System.IO.MatchCasing]::CaseInsensitive } } # Build EnumerationOptions $opt = [System.IO.EnumerationOptions]::new() $opt.IgnoreInaccessible = $IgnoreInaccessible $opt.MatchCasing = $MatchCasing $opt.MatchType = $MatchType $opt.ReturnSpecialDirectories = $IncludeSpecialDirectories.IsPresent # Depth implies recurse; otherwise use -Recurse switch if ($PSBoundParameters.ContainsKey('Depth')) { if ($Depth -lt 0) { throw 'Depth must be >= 0.' } $opt.MaxRecursionDepth = $Depth $opt.RecurseSubdirectories = $true } else { $opt.RecurseSubdirectories = $Recurse.IsPresent } # AttributesToSkip: 0 or aggregate bitflags if ($AttributesToSkip.Count -eq 1 -and $AttributesToSkip[0] -is [int] -and $AttributesToSkip[0] -eq 0) { $opt.AttributesToSkip = [System.IO.FileAttributes]0 } else { $fa = [System.IO.FileAttributes]0 foreach ($a in $AttributesToSkip) { $fa = $fa -bor ([System.IO.FileAttributes]$a) } $opt.AttributesToSkip = $fa } # Resolve one or many root paths (wildcards allowed) try { [ref]$prov = $null $rootPaths = $PSCmdlet.GetResolvedProviderPathFromPSPath($Path, $prov) if (-not $rootPaths -or $rootPaths.Count -eq 0) { throw "Path not found: $Path" } if ($PSBoundParameters.ContainsKey('Verbose')) { Write-Verbose "rootPaths: '$($rootPaths -join "', '")'" } } catch { Write-Error $_.Exception.Message return } # Precompute MinDepth helpers once $needsMin = ($MinDepth -gt 0) $sep1 = [IO.Path]::DirectorySeparatorChar $sep2 = [IO.Path]::AltDirectorySeparatorChar foreach ($root in $rootPaths) { # For backward-friendly verbose wording $absolutePath = $root if ($PSBoundParameters.ContainsKey('Verbose')) { Write-Verbose "absolutePath: '$absolutePath'" } # Choose API per root based on -As (String => Directory.*, typed => DirectoryInfo.*) $useTyped = ($As -ieq 'FileSystemInfo' -or $As -ieq 'FileInfo') if ($useTyped) { $di = [System.IO.DirectoryInfo]::new($root) switch ($Type) { 'Directory' { $enumerator = { param($pat) $di.EnumerateDirectories($pat, $opt) } } 'File' { $enumerator = { param($pat) $di.EnumerateFiles($pat, $opt) } } default { $enumerator = { param($pat) $di.EnumerateFileSystemInfos($pat, $opt) } } } } else { switch ($Type) { 'Directory' { $enumerator = { param($pat, $r) [System.IO.Directory]::EnumerateDirectories($r, $pat, $opt) } } 'File' { $enumerator = { param($pat, $r) [System.IO.Directory]::EnumerateFiles($r, $pat, $opt) } } default { $enumerator = { param($pat, $r) [System.IO.Directory]::EnumerateFileSystemEntries($r, $pat, $opt) } } } } # Optional one-time warning when using legacy -As FileInfo with non-File types (only when -Verbose) if ($As -ieq 'FileInfo' -and $Type -ne 'File' -and $PSBoundParameters.ContainsKey('Verbose')) { Write-Warning "Using -As FileInfo with -Type $Type returns DirectoryInfo for directories. Consider -As FileSystemInfo for clarity." } foreach ($pattern in $Name) { if ($PSBoundParameters.ContainsKey('Verbose')) { Write-Verbose "pattern: '$pattern'" } # Invoke enumerator (note: pass root when using Directory.*) $seq = if ($useTyped) { & $enumerator $pattern } else { & $enumerator $pattern $root } foreach ($item in $seq) { # Full path string for MinDepth calc depending on output mode $itemPath = if ($useTyped) { $item.FullName } else { $item } # Inline MinDepth filter relative to this root if ($needsMin) { $rel = [IO.Path]::GetRelativePath($root, $itemPath) if ($rel -eq '.' -or [string]::IsNullOrEmpty($rel)) { if ($MinDepth -gt 0) { continue } } else { $depth = 0 foreach ($ch in $rel.ToCharArray()) { if ($ch -eq $sep1 -or $ch -eq $sep2) { $depth++ } } if ($depth -lt $MinDepth) { continue } } } # Output if ($useTyped) { $item # FileInfo/DirectoryInfo as-is } else { $itemPath # full path string } } } } } Set-Alias -Name psfind -Value Find-Item -Force Set-Alias -Name ff -Value Find-Item -Force Set-Alias -Name fi -Value Find-Item -Force Set-Alias -Name search -Value Find-Item -Force |