|
@@ -0,0 +1,106 @@
|
|
|
+#!/usr/bin/env pwsh
|
|
|
+# Copyright (c) Daniel Sheffield 2025
|
|
|
+# All rights reserved
|
|
|
+#
|
|
|
+# Park is inspired by GNU Stow.
|
|
|
+#
|
|
|
+# It can't stow your files properly, but it will park them. Hopefully that is good enough.
|
|
|
+#
|
|
|
+# Park is designed to be a "drop-in" replacement to GNU Stow, with the following
|
|
|
+# limitations:
|
|
|
+# * Only long options are supported
|
|
|
+# * All options are ignored except:
|
|
|
+# * --stow
|
|
|
+# * --delete
|
|
|
+# * --restow
|
|
|
+# * --verbose (bool not int!)
|
|
|
+# * --simulate
|
|
|
+# * No tree folding by default (regardless of --nofolding option)
|
|
|
+#
|
|
|
+
|
|
|
+[CmdletBinding()]
|
|
|
+param(
|
|
|
+ [Parameter(Mandatory = $true)][string]$Dir,
|
|
|
+ [Parameter(Mandatory = $true)][string]$Package,
|
|
|
+ [Parameter(Mandatory = $true)][string]$Target,
|
|
|
+ [string]$Ignore,
|
|
|
+ [string]$Defer,
|
|
|
+ [string]$Override,
|
|
|
+ [switch]$Stow,
|
|
|
+ [switch]$Restow,
|
|
|
+ [switch]$Delete,
|
|
|
+ [switch]$Simulate,
|
|
|
+ [switch]$NoFolding,
|
|
|
+ [switch]$Adopt
|
|
|
+)
|
|
|
+
|
|
|
+$SourcePath = (Resolve-Path $Dir).Path
|
|
|
+$SourceRoot = [System.IO.Path]::GetFullPath($SourcePath)
|
|
|
+$SourceRoot = Join-Path $SourceRoot $Package
|
|
|
+$TargetRoot = [System.IO.Path]::GetFullPath($Target)
|
|
|
+#Write-Host "Parameters:"
|
|
|
+#foreach ($param in $MyInvocation.MyCommand.Parameters.Keys) {
|
|
|
+# $value = Get-Variable -Name $param -ValueOnly -ErrorAction SilentlyContinue
|
|
|
+# Write-Host "$param = $value"
|
|
|
+#}
|
|
|
+
|
|
|
+function Is-Under($child, $parent) {
|
|
|
+ $child = [System.IO.Path]::GetFullPath($child)
|
|
|
+ $parent = [System.IO.Path]::GetFullPath($parent)
|
|
|
+ return $child.StartsWith($parent, [System.StringComparison]::OrdinalIgnoreCase)
|
|
|
+}
|
|
|
+
|
|
|
+if ($Delete -or $Restow) {
|
|
|
+ Get-ChildItem -Path $SourceRoot -Recurse -Force | Where-Object {
|
|
|
+ -not $_.PSIsContainer -or $_.Attributes -match 'ReparsePoint'
|
|
|
+ } | ForEach-Object {
|
|
|
+ $relativePath = $_.FullName.Substring($SourceRoot.Length).TrimStart('\', '/')
|
|
|
+ $targetFile = Join-Path $TargetRoot $relativePath
|
|
|
+ Write-Verbose "$targetFile"
|
|
|
+ if (Test-Path $targetFile){
|
|
|
+ $targetFile = Get-Item $targetFile -Force
|
|
|
+ if ($targetFile.LinkType -eq 'SymbolicLink') {
|
|
|
+ $linkTarget = (Get-Item $targetFile.FullName -Force).Target
|
|
|
+ if ($linkTarget -and (Is-Under $linkTarget $SourceRoot)) {
|
|
|
+ Write-Verbose "Unlinking: $($targetFile.FullName) -> $linkTarget"
|
|
|
+ if (-not $Simulate) {
|
|
|
+ Remove-Item $targetFile.FullName -Force
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return
|
|
|
+}
|
|
|
+
|
|
|
+if ($Stow -or $Restow) {
|
|
|
+ Get-ChildItem -Path $SourceRoot -Recurse -Force | Where-Object {
|
|
|
+ -not $_.PSIsContainer -or $_.Attributes -match 'ReparsePoint'
|
|
|
+ } | ForEach-Object {
|
|
|
+ $relativePath = $_.FullName.Substring($SourceRoot.Length).TrimStart('\', '/')
|
|
|
+ $targetFile = Join-Path $TargetRoot $relativePath
|
|
|
+ $targetDir = Split-Path $targetFile
|
|
|
+ if (Test-Path $targetDir) {
|
|
|
+ if (-not (Get-Item $targetDir).PSIsContainer) {
|
|
|
+ Write-Verbose "Path '$targetDir' exists but is not a directory."
|
|
|
+ if (-not $Simulate) {
|
|
|
+ throw "Path '$targetDir' exists but is not a directory."
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ Write-Verbose "Creating dir: $targetDir"
|
|
|
+ if (-not $Simulate) {
|
|
|
+ New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Write-Verbose "Linking: $targetFile -> $($_.FullName)"
|
|
|
+ if (-not $Simulate) {
|
|
|
+ #New-Item -ItemType SymbolicLink -Path $targetFile -Target $_.FullName | Out-Null
|
|
|
+ #Write-Verbose 'cmd /c mklink "$($_.FullName)" "$targetFile" | Out-Null'
|
|
|
+ # cmd mklink works in Developer Mode, but New-Item does not
|
|
|
+ cmd /c mklink "$targetFile" "$($_.FullName)" | Out-Null
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return
|
|
|
+}
|