Public/New-DFShim.ps1
|
#Requires -Version 7.0 function New-DFShim { <# .SYNOPSIS Creates a .cmd shim that forwards invocations to a target executable, preserving the caller's working directory and exit code. .DESCRIPTION Generates a Windows .cmd batch file in the shims directory that, when invoked, runs the target executable with all forwarded arguments from the caller's own current directory -- so relative-path arguments resolve the way the user expects -- and correctly propagates the exit code. Put the shims directory on $PATH once and create shims as needed. Accepts a DotForge tool name (DB lookup) or an explicit -Target path. .PARAMETER Target Path to the target executable. Positional — can be passed without the parameter name. Bypasses tool DB lookup. When -Name is omitted, the shim name is derived from the target's basename (without extension). .PARAMETER Name Shim filename (without .cmd extension). When -Target is omitted, also used as the DotForge tool name to look up the executable path in the registry. Optional when -Target is given; derived from the target's basename if omitted. .PARAMETER ShimsPath Directory where the shim is written. Defaults to $DFConfig['ShimsPath'], then $HOME\.local\bin. .PARAMETER Force Overwrite an existing shim without error. .PARAMETER ToolsPath Override the tools directory (used in tests). .EXAMPLE New-DFShim 'C:\tools\grep\grep.exe' Creates $HOME\.local\bin\grep.cmd; name derived from the executable basename. .EXAMPLE New-DFShim -Name ripgrep Creates $HOME\.local\bin\ripgrep.cmd pointing at the ripgrep executable found via the DotForge tool registry. Warns if $HOME\.local\bin is not on PATH. .EXAMPLE New-DFShim 'C:\tools\myapp\myapp.exe' -Name myapp Creates a shim with an explicit name, bypassing name derivation. .EXAMPLE New-DFShim 'C:\tools\myapp\myapp.exe' -Force Overwrites an existing shim. .EXAMPLE New-DFShim -Name ripgrep -WhatIf Shows what would be created without writing any file. .OUTPUTS None #> [CmdletBinding(SupportsShouldProcess)] [OutputType([void])] param( [Parameter(Position = 0)] [string]$Target, [string]$Name, [string]$ShimsPath, [switch]$Force, [string]$ToolsPath ) # 0. Derive Name from Target basename if not provided; error if neither given if ($Target -and -not $Name) { $Name = [IO.Path]::GetFileNameWithoutExtension($Target) } elseif (-not $Target -and -not $Name) { Write-Error 'DotForge: Provide -Target (path to executable) or -Name (tool registry lookup).' return } # 1. Resolve shims directory $shimsDir = if ($ShimsPath) { $ShimsPath # Test the value, not the variable's existence: `$DFConfig = $null` leaves the # variable defined, and indexing into it throws "Cannot index into a null array". } elseif ($null -ne $Global:DFConfig -and $Global:DFConfig['ShimsPath']) { $Global:DFConfig['ShimsPath'] } else { Join-Path $HOME '.local' 'bin' } # Canonicalize (expands a ~ in ShimsPath / $DFConfig['ShimsPath'], collapses .., # normalizes separators) before creating the dir, checking PATH, and naming the shim. $shimsDir = ConvertTo-DFPath $shimsDir # 2. Create directory (idempotent) New-DFDirectory $shimsDir # 3. PATH check $onPath = $Env:PATH -split [IO.Path]::PathSeparator | Where-Object { $_ -and [IO.Path]::IsPathRooted($_) } | Where-Object { (ConvertTo-DFPath $_) -eq $shimsDir } if (-not $onPath) { Write-Warning "DotForge: '$shimsDir' is not on PATH — shims won't be invocable until it is added" } # 4. Resolve target executable $resolvedTarget = $null if ($Target) { if (-not (Test-Path $Target -PathType Leaf)) { Write-Error "DotForge: Target '$Target' does not exist or is not a file" return } $resolvedTarget = $Target } else { $dbArgs = if ($ToolsPath) { @{ ToolsPath = $ToolsPath } } else { @{} } $db = Import-DFToolDb @dbArgs if (-not $db.ContainsKey($Name)) { Write-Error "DotForge: Tool '$Name' not found in registry. Use -Target to specify the executable path." return } $executable = $db[$Name].executable $found = Get-Command $executable -ErrorAction Ignore if (-not $found) { Write-Error "DotForge: Tool '$Name' executable '$executable' not found on PATH. Is the tool installed?" return } $resolvedTarget = $found.Source } # 5. Shim existence check $shimPath = Join-Path $shimsDir "$Name.cmd" if ((Test-Path $shimPath) -and -not $Force -and -not $WhatIfPreference) { Write-Error "DotForge: Shim '$shimPath' already exists. Use -Force to overwrite." return } # 6. Write shim # No `cd` here: the shim must preserve the caller's own working directory # so relative-path arguments resolve the way the user expects, not against # the target executable's install directory. if ($PSCmdlet.ShouldProcess($shimPath, 'Create shim')) { $lines = @( '@echo off' 'setlocal' "`"$resolvedTarget`" %*" 'set "_exit=%ERRORLEVEL%"' 'endlocal & exit /b %_exit%' ) Set-Content -Path $shimPath -Value ($lines -join "`r`n") -Encoding ASCII -NoNewline Write-Verbose "DotForge: shim created → $shimPath" } } |