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`), call (`& path`) and Import-Module invocations whose path is a literal string or a $PSScriptRoot-based string resolving to the moved script. A relative path is resolved against the referencing script's own folder. It rewrites those paths as whole tokens, keeping each file's encoding and the original style ($PSScriptRoot-prefixed or .\-relative, and the / or \ separator). The moved script's own dot-source, call, Import-Module and `using module` paths are rebased too. HEURISTIC LIMIT: only literal and $PSScriptRoot-based string paths are resolved and rewritten. A string built from other variables (e.g. one rooted at $dir), or a string literal elsewhere in a script (e.g. a Join-Path argument), whose leaf matches the moved script is reported as a possible dynamic reference to verify by hand. A path assembled with no string naming the script 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 (otherwise a plain move, confirmed first or forced with -Force). It supports -WhatIf and does not need dotnet. .PARAMETER Path The .ps1 to move. Accepts a path string or a Get-ChildItem/Get-Item item from the pipeline, and rejects other object types. .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 When git is not installed, move with a plain PowerShell `Move-Item` without asking first. Without -Force it asks before falling back. The plain move 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 the rewrites of 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)] [Netscoot.PathInputTransform()] [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 } 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 } $scan = Get-PowerShellScriptReferences -File $f.FullName $referencers += @($scan.Paths | Where-Object { Test-PathEqual $_.Target $src }) $unresolvedRefs += @($scan.Dynamic | Where-Object { ($_.Raw -split '[\\/]')[-1] -eq $name }) } # The moved script's own paths (break when its location changes). $ownRefs = @((Get-PowerShellScriptReferences -File $src).Paths | Where-Object { $_.RawFollowing($newPath) -ne $_.Raw }) $refRels = @($referencers | ForEach-Object { $_.File }) $ownRels = @($ownRefs | ForEach-Object { $_.Raw }) Write-MovePlan -Cmdlet $PSCmdlet -Caption "Move-PowerShellScript $name $src -> $newPath" -Items ([ordered]@{ 'referencing scripts to fix' = $refRels 'own references to fix' = $ownRels }) 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. $pointAt = { param($Ref, $Target) [void]$Ref.PointAt($Target) } $followFile = { param($Ref, $NewFile) [void]$Ref.FollowFile($NewFile) } $items = @() foreach ($ref in $referencers) { $items += New-MoveItem -Description "referencer $(Split-Path -Leaf $ref.File): $($ref.Raw) -> $($ref.RawPointingAt($newPath))" ` -Reattach $pointAt -ReattachArgs @($ref, $newPath) } foreach ($ref in $ownRefs) { $items += New-MoveItem -Description "own reference: $($ref.Raw) -> $($ref.RawFollowing($newPath))" ` -Reattach $followFile -ReattachArgs @($ref, $newPath) } $move = { param($UseGit, $Src, $Dst, $Repository) Move-PathTracked -UseGit $UseGit -Source $Src -Destination $Dst -RepositoryRoot $Repository } $backup = @($referencers | ForEach-Object { $_.File }) + @($src) $planResult = Invoke-MovePlan -Caption "Move script $name" -Items $items -Move $move ` -MoveArgs @($ctx.UseGit, $src, $newPath, $repoFull) ` -BackupPath $backup -Rollback $move -RollbackArgs @($ctx.UseGit, $newPath, $src, $repoFull) ` -RepositoryRoot $repoFull -Command 'Move-PowerShellScript' -Engine 'powershell' -Source $src -Destination $newPath ` -UndoParams @{ Path = $newPath; Destination = $src; Force = [bool]$Force } -NoJournal:$NoJournal $performed = $true $skippedCount = $planResult.Skipped } New-MoveResult -TypeName 'Netscoot.ScriptMoveResult' -Engine 'powershell' -Source $src -Destination $newPath ` -Performed $performed -SkippedCount $skippedCount -Extra ([ordered]@{ ReferencersFixed = $referencers.Count OwnRefsFixed = $ownRefs.Count UnresolvedRefs = $unresolvedRefs.Count }) } } |