Netscoot.Core/Public/Move-PowerShellScript.ps1
|
function Move-PowerShellScript { <# .SYNOPSIS Move a standalone .ps1 script and fix the relative paths in scripts that dot-source or call it (and the moved script's own dot-source/call paths). .DESCRIPTION Finds references via the PowerShell AST: dot-source (`. path`) and call (`& path`) invocations whose path is a literal string or a $PSScriptRoot-based string resolving to the moved script. It rewrites those relative paths with precise, BOM-preserving edits, preserving the original style ($PSScriptRoot-prefixed or .\-relative). HEURISTIC LIMIT: only literal and $PSScriptRoot-based string paths are resolved and rewritten. A path that is a string containing other variables (e.g. "$dir\x.ps1") whose leaf matches the moved script is reported as a possible dynamic reference to verify by hand. A path built entirely from an expression (e.g. Join-Path ...) is not a string node and cannot be detected at all - grep to be sure. Treat the result as "fixed what could be proven," not "guaranteed complete." git is used when available (else confirmed plain-move fallback via -Force). -WhatIf supported; dotnet not required. .PARAMETER Path The .ps1 to move. Accepts pipeline input. .PARAMETER Destination New file path (or a folder, in which case the script keeps its name). .PARAMETER RepositoryRoot Root to scan for referencing scripts. Defaults to the enclosing git repository root. .PARAMETER Force Proceed with a plain file move when git is unavailable instead of aborting. The plain move is a PowerShell `Move-Item` (same on every platform) and does not preserve git history. .PARAMETER NoJournal Skip recording this move in the undo journal for this call, even when journaling is enabled (Undo-Netscoot will not see this move). .OUTPUTS Netscoot.ScriptMoveResult .EXAMPLE # Preview; rewrites dot-source/call paths in referencing scripts and the script's own refs Move-PowerShellScript -Path ./lib/helpers.ps1 -Destination ./shared/helpers.ps1 -WhatIf # Move it for real Move-PowerShellScript -Path ./lib/helpers.ps1 -Destination ./shared/helpers.ps1 # Limit the scan for referencing scripts to a specific root Move-PowerShellScript -Path ./lib/helpers.ps1 -Destination ./shared/helpers.ps1 -RepositoryRoot ./lib #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] [OutputType('Netscoot.ScriptMoveResult')] param( [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)] [Alias('FullName', 'PSPath')] [ValidateNotNullOrEmpty()] [string]$Path, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Destination, [string]$RepositoryRoot, [switch]$Force, [switch]$NoJournal ) process { $src = Resolve-FullPath $Path if (-not (Test-Path -LiteralPath $src -PathType Leaf)) { $PSCmdlet.WriteError([System.Management.Automation.ErrorRecord]::new( [System.IO.FileNotFoundException]::new("Script not found: $Path"), 'ScriptNotFound', [System.Management.Automation.ErrorCategory]::ObjectNotFound, $Path)) return } if ([System.IO.Path]::GetExtension($src) -ne '.ps1') { $PSCmdlet.WriteError([System.Management.Automation.ErrorRecord]::new( [System.ArgumentException]::new("Not a .ps1 script: $Path"), 'NotAScript', [System.Management.Automation.ErrorCategory]::InvalidArgument, $Path)) return } $name = Split-Path -Leaf $src # git mv semantics (shared by every mover): existing dir -> move into it; else rename. $newPath = Resolve-MoveTarget -Source $src -Destination $Destination if (Test-Path -LiteralPath $newPath) { $PSCmdlet.WriteError([System.Management.Automation.ErrorRecord]::new( [System.IO.IOException]::new("Destination already exists: $newPath"), 'DestinationExists', [System.Management.Automation.ErrorCategory]::ResourceExists, $newPath)) return } $newDir = Split-Path -Parent $newPath if (-not $RepositoryRoot) { $RepositoryRoot = Get-RepositoryRoot -StartPath (Split-Path -Parent $src) } $repoFull = Resolve-FullPath $RepositoryRoot # Referencers: scripts that dot-source/call the moved file. $referencers = @() $unresolvedRefs = @() foreach ($f in (Find-PowerShellFiles -Root $repoFull)) { if (Test-PathEqual $f.FullName $src) { continue } foreach ($r in (Get-PowerShellScriptReferences -File $f.FullName)) { if ($r.Unresolved) { if ((Split-Path $r.Raw -Leaf) -eq $name) { $unresolvedRefs += [pscustomobject]@{ File = $f.FullName; Raw = $r.Raw } } continue } if (Test-PathEqual $r.Abs $src) { $referencers += [pscustomobject]@{ File = $f.FullName; Raw = $r.Raw } } } } # The moved script's own dot-source/call paths (break when its location changes). $ownRefs = @(Get-PowerShellScriptReferences -File $src | Where-Object { -not $_.Unresolved }) Write-Verbose "Plan: move script $name $src -> $newPath" Write-Verbose " referencing scripts to fix : $($referencers.Count)" Write-Verbose " own references to fix : $($ownRefs.Count)" foreach ($u in $unresolvedRefs) { Write-Warning "Possible dynamic reference to $name in $($u.File): `"$($u.Raw)`" - could not resolve statically; verify by hand." } $performed = $false $skippedCount = 0 if ($PSCmdlet.ShouldProcess("$src -> $newPath", 'Move script and fix dot-source/call references')) { $ctx = Resolve-MoveContext -Cmdlet $PSCmdlet -Force:$Force -TargetForError $src if (-not $ctx) { return } # Reference fixes happen after the move; Reattach-only items (new raw computable now). $fixSb = { param($File, $Old, $New) [void](Set-RawFileReplacement -File $File -Old $Old -New $New) } $items = @() foreach ($ref in $referencers) { $newRaw = Get-NewScriptRaw -RefDir (Split-Path -Parent $ref.File) -TargetAbs $newPath -OldRaw $ref.Raw $items += New-MoveItem -Description "referencer $(Split-Path -Leaf $ref.File): $($ref.Raw) -> $newRaw" ` -Reattach $fixSb -ReattachArgs @($ref.File, $ref.Raw, $newRaw) } foreach ($own in $ownRefs) { $newRaw = Get-NewScriptRaw -RefDir $newDir -TargetAbs $own.Abs -OldRaw $own.Raw if ($newRaw -ne $own.Raw) { $items += New-MoveItem -Description "own reference: $($own.Raw) -> $newRaw" ` -Reattach $fixSb -ReattachArgs @($newPath, $own.Raw, $newRaw) } } $move = { param($UseGit, $Src, $Dst, $Repository) Move-PathTracked -UseGit $UseGit -Source $Src -Destination $Dst -RepositoryRoot $Repository } $planResult = Invoke-MovePlan -Caption "Move script $name" -Items $items -Move $move ` -MoveArgs @($ctx.UseGit, $src, $newPath, $repoFull) $performed = $true $skippedCount = $planResult.Skipped Register-MoveUndo -RepositoryRoot $repoFull -Command 'Move-PowerShellScript' -Engine 'powershell' ` -Source $src -Destination $newPath ` -UndoParams @{ Path = $newPath; Destination = $src; Force = [bool]$Force } -NoJournal:$NoJournal } New-MoveResult -TypeName 'Netscoot.ScriptMoveResult' -Engine 'powershell' -Source $src -Destination $newPath ` -Performed $performed -SkippedCount $skippedCount -Extra @{ ReferencersFixed = $referencers.Count OwnRefsFixed = $ownRefs.Count UnresolvedRefs = $unresolvedRefs.Count } } } |