Private/Get-AzLocalPipelineCustomiseMarkers.ps1

function Get-AzLocalPipelineCustomiseMarkers {
    <#
    .SYNOPSIS
        Parses BEGIN/END-AZLOCAL-CUSTOMIZE marker pairs out of a pipeline YAML
        text blob.
 
    .DESCRIPTION
        Private helper supporting Update-AzLocalPipelineExample. The marker
        convention is:
 
            <indent>#[<more #s>] BEGIN-AZLOCAL-CUSTOMIZE:<section>
            <body lines>
            <indent>#[<more #s>] END-AZLOCAL-CUSTOMIZE:<section>
 
        Both markers are plain YAML comments and therefore have zero runtime
        effect on either GitHub Actions or Azure DevOps. The <section> name
        is an identifier consisting of letters, digits, hyphens and
        underscores (regex: [A-Za-z0-9_-]+). It is expected to be unique per
        file; only the FIRST occurrence of a given <section> is kept if a
        file accidentally repeats it (a Write-Warning is emitted).
 
        The returned hashtable is keyed by <section> name. Each value is a
        PSCustomObject with:
            Name - <section>
            BeginLine - full text of the BEGIN line (no trailing newline)
            EndLine - full text of the END line (no trailing newline)
            Body - everything between BeginLine and EndLine, INCLUDING
                        the leading newline after BeginLine and the trailing
                        newline before EndLine, so reassembling
                        ($BeginLine + $Body + $EndLine) reconstructs the
                        original span verbatim.
            Index - 0-based start offset (start of BeginLine) into the
                        input text
            Length - total character length of the matched block
                        (BeginLine + Body + EndLine)
 
    .PARAMETER Text
        The full YAML text to scan. Use Get-Content -Raw or
        [IO.File]::ReadAllText to obtain it; line endings are preserved.
 
    .OUTPUTS
        [hashtable] keyed by section name (case-sensitive). Empty hashtable
        if no markers are found.
 
    .NOTES
        Added in v0.7.68.
    #>

    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [string]$Text
    )

    $result = @{}

    if ([string]::IsNullOrEmpty($Text)) {
        return $result
    }

    # The (?ms) flags enable multi-line mode (^/$ match line boundaries) and
    # single-line mode (. matches newlines) so the body capture can span
    # multiple lines.
    #
    # CRLF NOTE: in .NET regex, $ in multi-line mode matches "immediately
    # before \n" - which is BETWEEN the \r and \n in a \r\n line ending.
    # The character class [^\r\n]* therefore stops at \r, leaving the
    # engine positioned ON \r when it needs to match $ (which expects to be
    # AT the position right before \n). The explicit \r? makes the carriage
    # return optional but consumed, so the pattern matches both LF-only
    # files (what the bundled samples ship as) and CRLF files (what
    # Windows-checked-out copies often become after a git autocrlf
    # checkout). Without the \r? the parser silently returned zero markers
    # for every CRLF file - which would have silently broken
    # Update-AzLocalPipelineExample's entire marker-preservation feature
    # for any operator on a Windows checkout.
    #
    # Group 1 : full BEGIN line (everything from start of line up to and
    # including the section name and any trailing text, but NOT
    # the trailing \r or \n)
    # Group 2 : section name (back-referenced as \2 in the END line)
    # Group 3 : body (lazy - so adjacent marker blocks parse independently)
    # Group 4 : full END line
    $pattern = '(?ms)(^[^\r\n]*?BEGIN-AZLOCAL-CUSTOMIZE:([A-Za-z0-9_-]+)[^\r\n]*\r?)$(.*?)(^[^\r\n]*?END-AZLOCAL-CUSTOMIZE:\2[^\r\n]*\r?)$'
    $rx      = [regex]$pattern

    foreach ($m in $rx.Matches($Text)) {
        $name = $m.Groups[2].Value
        if ($result.ContainsKey($name)) {
            Write-Warning "Get-AzLocalPipelineCustomiseMarkers: duplicate AZLOCAL-CUSTOMIZE marker '$name' encountered; keeping the first occurrence at index $($result[$name].Index)."
            continue
        }
        $result[$name] = [PSCustomObject]@{
            Name      = $name
            BeginLine = $m.Groups[1].Value
            EndLine   = $m.Groups[4].Value
            Body      = $m.Groups[3].Value
            Index     = $m.Index
            Length    = $m.Length
        }
    }

    return $result
}