Private/FileEdit.ps1
|
using namespace System.Management.Automation.Language # Edits files in place. Only the changed text is touched, so comments stay. function Read-PsdRawFile { param([string] $Path, $Ctx) if (-not [IO.File]::Exists($Path)) { Add-PsdError $Ctx 'PSD100' "the file $Path doesn't exist."; return $null } try { $bytes = [IO.File]::ReadAllBytes($Path) } catch { Add-PsdError $Ctx 'PSD100' "the file $Path can't be read: $($_.Exception.InnerException.Message)" return $null } $bom = ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) # Strict: bytes that aren't UTF-8 would be replaced when read, and the damage written back. try { $text = (New-Object System.Text.UTF8Encoding $false, $true).GetString($bytes) } catch { Add-PsdError $Ctx 'PSD606' "the file can't be edited, because it isn't valid UTF-8. Save it as UTF-8." -File (New-PsdFileRec $Path $null) return $null } if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) } $nl = if ($text.Contains("`r`n")) { "`r`n" } else { "`n" } $tokens = $null; $errors = $null $ast = [Parser]::ParseInput($text, $Path, [ref]$tokens, [ref]$errors) $rec = New-PsdFileRec $Path $text if ($errors.Count -gt 0) { Add-PsdExtentError $Ctx 'PSD606' "the file can't be edited, because PowerShell can't read it: $($errors[0].Message.TrimEnd('.'))." $rec $errors[0].Extent return $null } $root = $null if ($ast.EndBlock -and $ast.EndBlock.Statements.Count -eq 1) { $root = Get-PsdStatementExpression $ast.EndBlock.Statements[0] } if ($root -isnot [HashtableAst]) { Add-PsdError $Ctx 'PSD606' "the file can't be edited, because it isn't one map: @{ ... }" -File $rec return $null } @{ Text = $text; Bom = $bom; NL = $nl; Ast = $ast; Root = $root; Tokens = $tokens; Rec = $rec } } function Get-PsdLineIndent { param([string] $Text, [int] $Offset) $start = $Text.LastIndexOf("`n", [Math]::Max(0, $Offset - 1)) if ($Offset -eq 0) { $start = -1 } $start++ $i = $start while ($i -lt $Text.Length -and ($Text[$i] -eq ' ' -or $Text[$i] -eq "`t")) { $i++ } $Text.Substring($start, $i - $start) } function Add-PsdIndent { param([string] $Text, [string] $Indent, [string] $NL) $lines = $Text -split '\r?\n' for ($i = 1; $i -lt $lines.Count; $i++) { $lines[$i] = $Indent + $lines[$i] } $lines -join $NL } # The items of an array expression. OnReject gets each statement that isn't a plain value. function Get-PsdArrayElements { param($Ast, [scriptblock] $OnReject = {}) $out = New-PsdList if ($Ast -is [ArrayLiteralAst]) { foreach ($x in $Ast.Elements) { $out.Add($x) }; return , $out } foreach ($st in $Ast.SubExpression.Statements) { $e = Get-PsdStatementExpression $st if ($null -eq $e) { & $OnReject $st } elseif ($e -is [ArrayLiteralAst]) { foreach ($x in $e.Elements) { $out.Add($x) } } else { $out.Add($e) } } , $out } # The edit that replaces a value. IndentAt is where the line that sets the indentation is. function New-PsdValueEdit { param($Raw, $Target, [int] $IndentAt, $Value, $Format, $Ctx) $vt = ConvertTo-PsdTextInternal $Value $Format 64 $Ctx @{ Start = $Target.Extent.StartOffset; End = $Target.Extent.EndOffset; Text = (Add-PsdIndent $vt (Get-PsdLineIndent $Raw.Text $IndentAt) $Raw.NL) } } # The edit that adds key number At of the path to Map. The rest of the path becomes nested maps. $null on errors. function New-PsdNewKeyEdit { param($Raw, $Map, $Segs, [int] $At, $Value, $Format, $Ctx) $text = $Raw.Text $nl = $Raw.NL $v = $Value for ($j = $Segs.Count - 1; $j -gt $At; $j--) { if ($Segs[$j].IsIndex) { Add-PsdError $Ctx 'PSD607' "$(Format-PsdSegs $Segs $j) doesn't exist, so [$($Segs[$j].Index)] can't be set." -File $Raw.Rec return $null } $m = [ordered]@{}; $m[$Segs[$j].Key] = $v; $v = $m } $keyText = Format-PsdKeyOut $Segs[$At].Key @{ Ctx = $Ctx; Path = (Format-PsdSegs $Segs $At) } if ($null -eq $keyText) { return $null } $hs = $Map.Extent.StartOffset $he = $Map.Extent.EndOffset if ($Map.KeyValuePairs.Count -gt 0) { $indent = Get-PsdLineIndent $text $Map.KeyValuePairs[0].Item1.Extent.StartOffset } else { $indent = (Get-PsdLineIndent $text $hs) + (' ' * $Format.Indent) } $entry = "$keyText = " + (Add-PsdIndent (ConvertTo-PsdTextInternal $v $Format 64 $Ctx) $indent $nl) if ($Map.Extent.StartLineNumber -ne $Map.Extent.EndLineNumber) { $close = $he - 1 $lineStart = $text.LastIndexOf("`n", $close - 1) + 1 if ($text.Substring($lineStart, $close - $lineStart).Trim().Length -eq 0) { return @{ Start = $lineStart; End = $lineStart; Text = "$indent$entry$nl" } } return @{ Start = $close; End = $close; Text = "$nl$indent$entry$nl$(Get-PsdLineIndent $text $hs)" } } if ($Map.KeyValuePairs.Count -eq 0) { return @{ Start = $hs; End = $he; Text = "@{ $entry }" } } $end = $Map.KeyValuePairs[$Map.KeyValuePairs.Count - 1].Item2.Extent.EndOffset @{ Start = $end; End = $end; Text = "; $entry" } } function Update-PsdFileValue { param([string] $Path, $Segs, $Value, $Format, $Ctx) $raw = Read-PsdRawFile $Path $Ctx if ($null -eq $raw) { return $false } $text = $raw.Text $Format.LineEnding = if ($raw.NL -eq "`r`n") { 'CRLF' } else { 'LF' } $fail = { param($message) Add-PsdError $Ctx 'PSD607' $message -File $raw.Rec } $cur = $raw.Root $edit = $null for ($i = 0; $i -lt $Segs.Count; $i++) { $seg = $Segs[$i] $last = ($i -eq $Segs.Count - 1) $under = Format-PsdSegs $Segs $i $shownUnder = if ($under) { $under } else { 'the file' } if ($cur -is [HashtableAst]) { if ($seg.IsIndex) { & $fail "$shownUnder is a map, so [$($seg.Index)] can't be used on it."; return $false } $pair = $null foreach ($p in $cur.KeyValuePairs) { if ($p.Item1 -is [StringConstantExpressionAst] -and $p.Item1.Value -ieq $seg.Key) { $pair = $p; break } } if (-not $pair) { $edit = New-PsdNewKeyEdit $raw $cur $Segs $i $Value $Format $Ctx; break } $expr = Get-PsdStatementExpression $pair.Item2 if ($null -eq $expr) { & $fail "$(Format-PsdSegs $Segs ($i + 1)) isn't a plain value, so it can't be changed."; return $false } if ($last) { $edit = New-PsdValueEdit $raw $expr $pair.Item1.Extent.StartOffset $Value $Format $Ctx; break } $cur = $expr continue } if ($cur -is [ArrayExpressionAst] -or $cur -is [ArrayLiteralAst]) { if (-not $seg.IsIndex) { & $fail "$shownUnder is a list, so .$($seg.Key) can't be used on it."; return $false } $els = Get-PsdArrayElements $cur $idx = $seg.Index if ($idx -lt 0) { $idx += $els.Count } if ($idx -lt 0 -or $idx -ge $els.Count) { & $fail "$shownUnder has $($els.Count) items, so [$($seg.Index)] doesn't exist."; return $false } if ($last) { $edit = New-PsdValueEdit $raw $els[$idx] $els[$idx].Extent.StartOffset $Value $Format $Ctx; break } $cur = $els[$idx] continue } & $fail "$shownUnder is a single value, so it has nothing inside it to change." return $false } if ($Ctx.Errors.Count -gt 0 -or $null -eq $edit) { return $false } $new = $text.Substring(0, $edit.Start) + $edit.Text + $text.Substring($edit.End) Write-PsdTextFile $Path $new $raw.Bom $Ctx } # Re-indents by nesting depth and trims trailing spaces. Comments and strings stay. function Format-PsdFileText { param([string] $Path, $Format, [bool] $BomGiven, $Ctx) $raw = Read-PsdRawFile $Path $Ctx if ($null -eq $raw) { return $false } $text = $raw.Text $nl = $raw.NL switch ($Format.LineEnding) { 'LF' { $nl = "`n" } 'CRLF' { $nl = "`r`n" } } $opens = @('AtCurly', 'LCurly', 'AtParen', 'LParen', 'DollarParen', 'LBracket') $closes = @('RCurly', 'RParen', 'RBracket') $tokens = New-PsdList foreach ($t in $raw.Tokens) { if ($t.Kind -ne 'NewLine' -and $t.Kind -ne 'EndOfInput' -and $t.Kind -ne 'LineContinuation') { $tokens.Add($t) } } $lines = $text -split '\r?\n' $starts = New-Object 'int[]' $lines.Count $off = 0 for ($i = 0; $i -lt $lines.Count; $i++) { $starts[$i] = $off $off += $lines[$i].Length if ($off -lt $text.Length -and $text[$off] -eq "`r") { $off++ } $off++ } $out = New-PsdList $depth = 0 $ti = 0 $info = New-PsdList for ($i = 0; $i -lt $lines.Count; $i++) { $s = $starts[$i] $inside = $false while ($ti -lt $tokens.Count -and $tokens[$ti].Extent.StartOffset -lt $s) { $t = $tokens[$ti] if ($opens -contains [string]$t.Kind) { $depth++ } elseif ($closes -contains [string]$t.Kind) { $depth = [Math]::Max(0, $depth - 1) } if ($t.Extent.EndOffset -gt $s) { $inside = $true } $ti++ } # A token that continues past this line keeps its own line breaks. if ($inside) { $out.Add($lines[$i]); $info.Add($null); continue } $trim = $lines[$i].Trim() if ($trim.Length -eq 0) { $out.Add(''); $info.Add($null); continue } $d = $depth $first = $null; $second = $null if ($ti -lt $tokens.Count -and $tokens[$ti].Extent.StartLineNumber -eq ($i + 1)) { $first = $tokens[$ti] if ($ti + 1 -lt $tokens.Count -and $tokens[$ti + 1].Extent.StartLineNumber -eq ($i + 1)) { $second = $tokens[$ti + 1] } } if ($first -and $closes -contains [string]$first.Kind) { $d = [Math]::Max(0, $d - 1) } $out.Add((' ' * ($Format.Indent * $d)) + $trim) $isKey = $first -and $second -and [string]$second.Kind -eq 'Equals' -and $d -gt 0 if ($isKey) { $info.Add(@{ Depth = $d; Key = $first.Extent.Text }) } else { $info.Add($null) } } if ($Format.AlignEquals) { $i = 0 while ($i -lt $out.Count) { if ($null -eq $info[$i]) { $i++; continue } $j = $i $width = 0 while ($j -lt $out.Count -and $info[$j] -and $info[$j].Depth -eq $info[$i].Depth) { $width = [Math]::Max($width, $info[$j].Key.Length); $j++ } for ($k = $i; $k -lt $j; $k++) { $line = $out[$k] $pad = ' ' * ($Format.Indent * $info[$k].Depth) $rest = $line.Substring($pad.Length + $info[$k].Key.Length).TrimStart() if ($rest.StartsWith('=')) { $out[$k] = $pad + $info[$k].Key.PadRight($width) + ' ' + $rest } } $i = $j } } $new = $out -join $nl $bom = $raw.Bom if ($BomGiven) { $bom = $Format.Bom } Write-PsdTextFile $Path $new $bom $Ctx } |