Functions/GenXdev.FileSystem/Find-Item.ps1

################################################################################
<#
.SYNOPSIS
Searches for file- or directory- names with optionally filtering regex content matching
 
.DESCRIPTION
Searches for file- or directory- names, optionally performs a regular expression
match within the content of each matched file.
 
.PARAMETER SearchMask
Specify the file name or pattern to search for. Default is "*".
 
.PARAMETER Pattern
Specify the pattern to search within the files. Default is ".*".
 
.PARAMETER AllDrives
Search all drives.
 
.PARAMETER Directory
Search for directories only.
 
.PARAMETER PassThru
Pass through the objects to the pipeline.
 
.EXAMPLE
# Find all files with the .txt extension in the current directory and its subdirectories
Find-Item -SearchMask "*.txt"
 
# or in short
l *.txt
 
.EXAMPLE
# Find all files with that have the word "translation" in their name
Find-Item -SearchMask "*translation*"
 
# or in short
l *translation*
 
.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 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
 
.NOTES
Assuming c:\temp exists;
 
'Find-Item c:\temp\'
    would search the whole content of directory 'temp' for any file or directory with the name 'temp'
 
'Find-Item c:\temp'
    would search the whole C drive for any file or directory with the name 'temp'
 
'Find-Item temp -AllDrives'
    would search the all drives for any file or directory with the name 'temp'
so would:
    'Find-Item c:\temp -AllDrives'
#>

function Find-Item {

    [CmdletBinding(DefaultParameterSetName = "Default")]
    [Alias("l")]

    param(
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            Position = 0,
            HelpMessage = "File name or pattern to search for. Default is '*'"
        )]
        [Alias("like", "l")]
        [PSDefaultValue(Value = "*")]
        [string] $SearchMask = "*",
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            Position = 1,
            ParameterSetName = 'WithPattern',
            HelpMessage = "Regular expression pattern to search within matched files"
        )]
        [Alias("mc", "matchcontent")]
        [PSDefaultValue(Value = ".*")]
        [string] $Pattern = ".*",
        ###############################################################################
        [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,
            HelpMessage = "Output matched items as objects rather than strings"
        )]
        [switch] $PassThru
        ###############################################################################
    )

    begin {

        $SearchMask = $SearchMask.Trim()
        if ($SearchMask -eq [string]::Empty) {

            $SearchMask = ".\*"
        }
        $SearchMask = $SearchMask.Trim().Replace("\", [IO.Path]::DirectorySeparatorChar).Replace("/", [IO.Path]::DirectorySeparatorChar).Replace([IO.Path]::DirectorySeparatorChar + [IO.Path]::DirectorySeparatorChar, [IO.Path]::DirectorySeparatorChar)

        if ($SearchMask.EndsWith([IO.Path]::DirectorySeparatorChar)) {

            $SearchMask += "*"
        }

        # expand search mask path to full path
        $SearchMask = Expand-Path $SearchMask

        # get current directory for relative path handling
        $location = Get-Location | ForEach-Object Path

        # helper function to search file content
        function Search-FileContent {
            param (
                [string] $FilePath,
                [string] $Pattern
            )

            return Select-String -Path $FilePath -Pattern $Pattern
        }

        $SearchMask = Expand-Path $SearchMask
    }

    process {

        # output blank line unless passing through objects
        if (-not $PassThru) {
            "" | Out-Host
        }

        # searching with content pattern
        if ((-not $Directory) -and ($Pattern -ne ".*") -and
            (-not [string]::IsNullOrWhiteSpace($Pattern))) {

            # searching all drives
            if ($AllDrives) {

                # get all drives and process in parallel
                Get-PSDrive -ErrorAction SilentlyContinue |
                ForEach-Object -ThrottleLimit 8 -Parallel {

                    # extract filename from search mask
                    $file = [IO.Path]::GetFileName($SearchMask)
                    $filter = [string]::IsNullOrEmpty($file) ? "*" : $file

                    try {
                        # skip non-filesystem providers
                        if ($PSItem.Provider.Name -ne "FileSystem") { return }

                        # search files matching name filter
                        Get-ChildItem "$($PSItem.Root)" -File:($Directory -eq $false) `
                            -ErrorAction SilentlyContinue `
                            -Directory:($Directory -eq $true) -Recurse |
                        Where-Object -Property Name -Like $filter |
                        ForEach-Object {

                            # check file content for pattern
                            if (Search-FileContent -FilePath $PSItem.FullName `
                                    -Pattern $Pattern) {

                                if ($PassThru) {
                                    $PSItem
                                }
                                else {
                                    # return relative or full path
                                    if ($PSItem.FullName.StartsWith($location +
                                            [IO.Path]::DirectorySeparatorChar)) {
                                        ".$($PSItem.FullName.Substring($location.Length))"
                                    }
                                    else {
                                        $PSItem.FullName
                                    }
                                }
                            }
                        }
                    }
                    catch {}
                }
                return
            }

            # regular content search in current location
            Get-ChildItem $SearchMask -File -Recurse | ForEach-Object {
                if (($Pattern -eq ".*") -or
                    (Search-FileContent -FilePath $PSItem.FullName -Pattern $Pattern)) {

                    if ($PassThru) {
                        $PSItem
                        return
                    }

                    if ($PSItem.FullName.StartsWith($location +
                            [IO.Path]::DirectorySeparatorChar)) {
                        ".$($PSItem.FullName.Substring($location.Length))"
                    }
                    else {
                        $PSItem.FullName
                    }
                }
            }
            return
        }

        # searching all drives without content pattern
        if ($AllDrives) {
            Get-PSDrive -ErrorAction SilentlyContinue |
            ForEach-Object -ThrottleLimit 8 -Parallel {
                try {
                    if ($PSItem.Provider.Name -ne "FileSystem") { return }

                    $file = [IO.Path]::GetFileName($SearchMask)
                    $filter = [string]::IsNullOrEmpty($file) ? "*" : $file

                    Get-ChildItem "$($PSItem.Root)" -File:($Directory -eq $false) `
                        -ErrorAction SilentlyContinue `
                        -Directory:($Directory -eq $true) -Recurse |
                    Where-Object -Property Name -Like $filter |
                    ForEach-Object {
                        if ($PassThru) {
                            $PSItem
                            return
                        }
                        $PSItem.FullName
                    }
                }
                catch {}
            }
            return
        }

        # regular file/directory search without content pattern
        $dir = [IO.Path]::GetDirectoryName($SearchMask)
        if ([string]::IsNullOrEmpty($dir)) {
            $dir = ".$([IO.Path]::DirectorySeparatorChar)"
        }

        $file = [IO.Path]::GetFileName($SearchMask)
        $search = [string]::IsNullOrEmpty($dir) ?
            ([string]::IsNullOrEmpty($file) ? "*" : $file) : $dir
        $filter = [string]::IsNullOrEmpty($file) ? "*" : $file

        Get-ChildItem $search -File:($Directory -eq $false) `
            -ErrorAction SilentlyContinue `
            -Directory:($Directory -eq $true) -Recurse |
        Where-Object -Property Name -Like $filter |
        ForEach-Object {
            if ($PassThru) {
                $PSItem
                return
            }

            if ($PSItem.FullName.StartsWith($location +
                    [IO.Path]::DirectorySeparatorChar)) {
                ".$($PSItem.FullName.Substring($location.Length))"
            }
            else {
                $PSItem.FullName
            }
        }

        # output blank line unless passing through objects
        if (-not $PassThru) {
            "" | Out-Host
        }
    }

    end {
    }
}