Public/Edit-StringAtLine.ps1
|
<# .SYNOPSIS Replaces the first occurrence of a string in pipeline or parameter input, optionally preserving line structure for multiline replacement. .DESCRIPTION Edit-StringAtLine finds the first match of StringToMatch in the input string and replaces it with ReplacementString. When the replacement spans multiple lines, -Multiline outputs each resulting line separately; otherwise a single string is output. Useful for in-memory or pipeline text editing when you need to replace a pattern once and preserve alignment. .PARAMETER InputString The string to search and replace in. Accepts pipeline input and pipeline-by-property-name (e.g. from Get-Content -Raw). .PARAMETER StringToMatch The regular expression to find. Only the first match is replaced. Escape literal text with [regex]::Escape() when it contains regex characters. .PARAMETER ReplacementString The literal text to replace the match with (substitutions such as $1 are not expanded). Can be multiline; additional lines are indented to the column where the match started. .PARAMETER Multiline When set, outputs the result as one string per line. When not set, outputs a single string with "`r`n" between lines. If there is no match the input is output unchanged (split into lines when -Multiline is set). .EXAMPLE Get-Content -Path 'C:\Script.ps1' -Raw | Edit-StringAtLine -StringToMatch 'Version = ''1.0''' -ReplacementString 'Version = ''2.0''' Replaces the first occurrence of the version string in the file content and outputs the modified string. .EXAMPLE $text = "Line one`nLine two`nLine three" $text | Edit-StringAtLine -StringToMatch 'Line two' -ReplacementString "Line two`n indented" -Multiline Replaces "Line two" with two lines and outputs each line separately; the second line is indented to the match position. .INPUTS System.String. Input string can be piped by value or by property name (InputString). .OUTPUTS System.String. The modified string, or when -Multiline is set, one string per line. .NOTES Author: TheCodeSaiyan Uses [regex]::Match; not compatible with Constrained Language Mode if that restricts the Regex type. #> function Edit-StringAtLine { [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [AllowEmptyString()] [string] $InputString, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $StringToMatch, [Parameter(Mandatory = $true)] [AllowEmptyString()] [string] $ReplacementString, [switch] $Multiline ) begin { $TelemetryArgs = @{ ModuleName = $MyInvocation.MyCommand.Module.Name ModuleVersion = [string]$MyInvocation.MyCommand.Module.Version CommandName = $MyInvocation.MyCommand.Name ExecutionID = [guid]::NewGuid().ToString() } Invoke-TelemetryCollection @TelemetryArgs -Stage Start -ClearTimer $TelemetryFailed = $false } process { try { Write-Verbose "Input string: $($InputString.Length) characters long." $match = [regex]::Match($InputString, $StringToMatch) if (-not $match.Success) { Write-Verbose "No match for '$StringToMatch'; the input is returned unchanged." if ($Multiline) { return $InputString -split "`r?`n" } return $InputString } # Column of the match on its line, used to indent additional replacement lines $lineStart = $InputString.LastIndexOf("`n", [Math]::Max($match.Index - 1, 0)) if ($match.Index -eq 0 -or $lineStart -lt 0) { $lineStart = 0 } else { $lineStart += 1 } $positionOnLine = $match.Index - $lineStart Write-Verbose "Match found at position $($match.Index) (column $positionOnLine)." $replacementLines = @($ReplacementString -split "`r?`n") for ($i = 1; $i -lt $replacementLines.Count; $i++) { $replacementLines[$i] = (' ' * $positionOnLine) + $replacementLines[$i] } $result = $InputString.Substring(0, $match.Index) + ($replacementLines -join "`n") + $InputString.Substring($match.Index + $match.Length) $resultLines = $result -split "`r?`n" if ($Multiline) { $resultLines } else { $resultLines -join "`r`n" } } catch { if (-not $TelemetryFailed) { $TelemetryFailed = $true Invoke-TelemetryCollection @TelemetryArgs -Stage End -Failed $true -Exception $_ } throw } } end { if (-not $TelemetryFailed) { Invoke-TelemetryCollection @TelemetryArgs -Stage End } } } |