Public/Search-PatternInFile.ps1

#!/usr/bin/env pwsh
Set-StrictMode -Version Latest

function Search-PatternInFile {
    <#
    .SYNOPSIS
        Searches the files under a path for a regular expression.

    .DESCRIPTION
        Recurses the path, including hidden and system files, and searches each file
        with Select-String on parallel threads. Files are read as UTF-8 and every match
        on a line is returned. Files and directories that can't be read are skipped.

        When -Pattern is not given, searches for IPv4 addresses.

    .PARAMETER Path
        The file or directory to search. Wildcards are accepted. Defaults to the
        current location. Accepts paths from the pipeline.

    .PARAMETER Pattern
        The regular expression to search for. Defaults to a pattern that matches IPv4
        addresses.

    .PARAMETER ThrottleLimit
        The maximum number of files searched at once, from 1 through 255. Defaults to
        75% of the processors, rounded down, and at least 1.

    .INPUTS
        System.String. You can pipe paths to this function.

    .OUTPUTS
        System.Management.Automation.PSCustomObject. One object per file with at least
        one match, with the file path, the match count, and the line number, line, and
        matched values of each match.

    .EXAMPLE
        Search-PatternInFile -Path ./Doc/PwshBestPractice/PowershellExamples.md -Pattern 'Get-FileHash'

        Finds the Microsoft help examples that use Get-FileHash.

    .EXAMPLE
        Search-PatternInFile -Path ./Config

        Finds the IPv4 addresses in every file under ./Config.

    .EXAMPLE
        './Public', './Private' | Search-PatternInFile -Pattern 'using namespace' | Select-Object -ExpandProperty Path

        Lists the files in Public and Private that still have a using namespace line.
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(ValueFromPipeline,
            HelpMessage = 'Enter the file or directory to search. Wildcards are accepted. Accepts paths from the pipeline.')]
        [ValidateNotNullOrEmpty()]
        [string]$Path,

        [Parameter(HelpMessage = 'Enter the regular expression to search for.')]
        [ValidateNotNullOrEmpty()]
        [string]$Pattern,

        [Parameter(HelpMessage = 'Enter the maximum number of files searched at once (1-255).')]
        [ValidateRange(1, 255)]
        [int]$ThrottleLimit
    )
    begin {
        if (-not $PSBoundParameters.ContainsKey('ThrottleLimit')) {
            $ThrottleLimit = Get-DefaultThrottleLimit
        }

        if (-not $PSBoundParameters.ContainsKey('Pattern')) {
            $Pattern = '(?<![\d.])(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}(?![\d.])'
        }
    }
    process {
        if (-not $PSBoundParameters.ContainsKey('Path')) {
            $Path = (Get-Location).Path
        }

        # Throws when nothing matches the path. A wildcard can resolve to several paths.
        $resolvedPath = (Resolve-Path -Path $Path -ErrorAction Stop).Path

        $searchParams = @{
            ThrottleLimit = $ThrottleLimit
            Parallel      = {
                $selectParams = @{
                    LiteralPath = $_.FullName
                    Pattern     = $using:Pattern
                    Encoding    = 'utf8'
                    AllMatches  = $true
                    ErrorAction = 'SilentlyContinue'
                }
                $matchInfo = @(Select-String @selectParams)
                if ($matchInfo.Count -gt 0) {
                    $values = @{ Name = 'Values'; Expression = { , @($_.Matches.Value) } }
                    [PSCustomObject]@{
                        Path       = $_.FullName
                        MatchCount = @($matchInfo.Matches).Count
                        Matches    = @($matchInfo | Select-Object -Property LineNumber, Line, $values)
                    }
                }
            }
        }
        Get-ChildItem -Path $resolvedPath -Recurse -File -Force -ErrorAction SilentlyContinue |
            ForEach-Object @searchParams
    }
}