Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat(shim): enhance support for managing local executable shims
- `scoop shim add/rm` now remembers manually added shims in the config file.
- `scoop shim list` adds a `--added` option to show only manually added shims; output uses `Format-Table`.
- `scoop shim info` and `scoop shim list` display an `isAdded` field to indicate if a shim was manually added.
- `scoop shim alter` now supports paths to non-scoop-installed executables.
  • Loading branch information
gigberg committed Sep 30, 2025
commit 8b637beea7a45c02862ede4a578689911de2862a
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ test/tmp/*
*~
TestResults.xml
supporting/sqlite/*
.vscode/*
5 changes: 5 additions & 0 deletions lib/core.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -835,8 +835,13 @@ function get_app_name($path) {
$appName = $Matches[1].ToLower()
} elseif ((Test-Path (appsdir $true)) -and ($path -match "$([Regex]::Escape($(Convert-Path (appsdir $true))))[/\\]([^/\\]+)")) {
$appName = $Matches[1].ToLower()
} elseif ($path -match '[/\\]([^/\\]+?)(?:\.[^/\\"]+?)') {
# Fallback: extract the executable file name (with extension if present): [/\\]([^/\\]+?)(?:\.?[^./\\]+?)`"
$appName = $Matches[1].ToLower()
Write-Host $path + '1' + $appName
} else {
$appName = ''
Write-Host $path + '2' + $appName
}
return $appName
}
Expand Down
147 changes: 121 additions & 26 deletions libexec/scoop-shim.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
#
# scoop shim rm <shim_name> [<shim_name>...]
#
# To list all shims or matching shims, use the 'list' subcommand:
# To list all shims or matching shims, use the 'list' subcommand (`-added` to show shims added by user in config):
#
# scoop shim list [<regex_pattern>...]
# scoop shim list -added [<regex_pattern>...]
#
# To show a shim's information, use the 'info' subcommand:
#
Expand All @@ -31,11 +31,67 @@
#
# scoop shim add myapp 'D:\path\myapp.exe' '--' myapp_args --global

# Main updated features:
# 1) `scoop shim add/rm` persist shims in the config file,
# 2) `scoop shim list` supports the --added [pattern|optional] option to display or search only user-added shims.

param($SubCommand)

. "$PSScriptRoot\..\lib\getopt.ps1"
. "$PSScriptRoot\..\lib\install.ps1" # for rm_shim
. "$PSScriptRoot\..\lib\system.ps1" # 'Add-Path' (indirectly)
. "$PSScriptRoot\..\apps\scoop\current\lib\getopt.ps1"
. "$PSScriptRoot\..\apps\scoop\current\lib\core.ps1" # for config related ops
. "$PSScriptRoot\..\apps\scoop\current\lib\install.ps1" # for rm_shim
. "$PSScriptRoot\..\apps\scoop\current\lib\system.ps1" # 'Add-Path' (indirectly)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Verify hardcoded relative paths.

The import paths use hardcoded relative paths ($PSScriptRoot\..\apps\scoop\current\...) that assume a specific directory structure. This will break if:

  • The script is moved to a different location
  • Scoop is installed in a non-standard location
  • The script is invoked from a different context

Run the following script to check if there are standard path resolution patterns used elsewhere in the codebase:


🏁 Script executed:

#!/bin/bash
# Search for how other libexec scripts resolve paths to lib files
rg -n -C3 '^\. .*(lib|supporting)/' libexec/ | head -50

Length of output: 0


🏁 Script executed:

#!/bin/bash
# List all scripts under libexec to identify candidates
find libexec -maxdepth 1 -type f -print

# Search for PSScriptRoot usage to find alternative resolution patterns
rg -n '\$PSScriptRoot' -C2 libexec/

Length of output: 16025


🏁 Script executed:

#!/bin/bash
# Verify if apps/scoop/current/lib exists in the repo
find . -maxdepth 5 -type d -path "./apps/scoop/current/lib"
# If found, list its contents
if [ -d "apps/scoop/current/lib" ]; then
  ls -1 apps/scoop/current/lib
else
  echo "apps/scoop/current/lib not found"
fi

Length of output: 32


Derive the scoop install path dynamically instead of hardcoded relative imports in scoop-shim.ps1. Lines 40–43 import from $PSScriptRoot\..\apps\scoop\current\lib\*.ps1, which assumes a fixed install layout (and diverges from other libexec scripts that load from ..\lib). Refactor to compute the scoop root at runtime (e.g. via an environment variable or config) or clearly document the required directory structure. [lines 40–43]



# Read the configuration of manually added shims
function Get-AddedShimsConfig {
$added = get_config 'added'
if ($null -eq $added) {
return [PSCustomObject]@{}
}
return $added
}

# Add shim to config
function Add-ShimToConfig($shimName, $global, $commandPath, $commandArgs) {
$added = Get-AddedShimsConfig

# Use new structure: shimName as key, global as a boolean field
$shimInfo = [PSCustomObject]@{
CommandPath = $commandPath
CommandArgs = $commandArgs -join ' '
AddedDate = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss')
global = $global
}

# If shimName does not exist, add it
if (-not $added.PSObject.Properties[$shimName]) {
$added | Add-Member -MemberType NoteProperty -Name $shimName -Value $shimInfo
} else {
$added.$shimName = $shimInfo
}

set_config 'added' $added | Out-Null
}
Comment on lines +56 to +75
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid dotted property access for arbitrary shim names; use PSObject property bag.

Shim names may contain dots/hyphens. $added.$shimName will break. Use PSObject.Properties[$name] for both add and update. Also store args as an array to preserve tokens.

 function Add-ShimToConfig($shimName, $global, $commandPath, $commandArgs) {
     $added = Get-AddedShimsConfig
 
     # Use new structure: shimName as key, global as a boolean field
     $shimInfo = [PSCustomObject]@{
         CommandPath = $commandPath
-        CommandArgs = $commandArgs -join ' '
+        CommandArgs = $commandArgs
         AddedDate = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss')
         global = $global
     }
 
-    # If shimName does not exist, add it
-    if (-not $added.PSObject.Properties[$shimName]) {
-        $added | Add-Member -MemberType NoteProperty -Name $shimName -Value $shimInfo
-    } else {
-        $added.$shimName = $shimInfo
-    }
+    $prop = $added.PSObject.Properties[$shimName]
+    if ($prop) {
+        $prop.Value = $shimInfo
+    } else {
+        $added | Add-Member -MemberType NoteProperty -Name $shimName -Value $shimInfo
+    }
 
     set_config 'added' $added | Out-Null
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function Add-ShimToConfig($shimName, $global, $commandPath, $commandArgs) {
$added = Get-AddedShimsConfig
# Use new structure: shimName as key, global as a boolean field
$shimInfo = [PSCustomObject]@{
CommandPath = $commandPath
CommandArgs = $commandArgs -join ' '
AddedDate = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss')
global = $global
}
# If shimName does not exist, add it
if (-not $added.PSObject.Properties[$shimName]) {
$added | Add-Member -MemberType NoteProperty -Name $shimName -Value $shimInfo
} else {
$added.$shimName = $shimInfo
}
set_config 'added' $added | Out-Null
}
function Add-ShimToConfig($shimName, $global, $commandPath, $commandArgs) {
$added = Get-AddedShimsConfig
# Use new structure: shimName as key, global as a boolean field
$shimInfo = [PSCustomObject]@{
CommandPath = $commandPath
CommandArgs = $commandArgs
AddedDate = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss')
global = $global
}
$prop = $added.PSObject.Properties[$shimName]
if ($prop) {
$prop.Value = $shimInfo
} else {
$added | Add-Member -MemberType NoteProperty -Name $shimName -Value $shimInfo
}
set_config 'added' $added | Out-Null
}


# Remove shim from config
function Remove-ShimFromConfig($shimName, $global) {
$added = Get-AddedShimsConfig

# Check if shim exists and matches global flag
if ($added.PSObject.Properties[$shimName] -and $added.$shimName.global -eq $global) {
$added.PSObject.Properties.Remove($shimName)
set_config 'added' $added | Out-Null
}
}
Comment on lines +77 to +86
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Property removal must not rely on dotted access; compare flag via property value.

Direct $added.$shimName.global fails for keys with special chars. Use the property bag value.

 function Remove-ShimFromConfig($shimName, $global) {
     $added = Get-AddedShimsConfig
 
     # Check if shim exists and matches global flag
-    if ($added.PSObject.Properties[$shimName] -and $added.$shimName.global -eq $global) {
-        $added.PSObject.Properties.Remove($shimName)
+    $prop = $added.PSObject.Properties[$shimName]
+    if ($prop -and $prop.Value.global -eq $global) {
+        [void]$added.PSObject.Properties.Remove($shimName)
         set_config 'added' $added | Out-Null
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Remove shim from config
function Remove-ShimFromConfig($shimName, $global) {
$added = Get-AddedShimsConfig
# Check if shim exists and matches global flag
if ($added.PSObject.Properties[$shimName] -and $added.$shimName.global -eq $global) {
$added.PSObject.Properties.Remove($shimName)
set_config 'added' $added | Out-Null
}
}
# Remove shim from config
function Remove-ShimFromConfig($shimName, $global) {
$added = Get-AddedShimsConfig
# Check if shim exists and matches global flag
$prop = $added.PSObject.Properties[$shimName]
if ($prop -and $prop.Value.global -eq $global) {
[void]$added.PSObject.Properties.Remove($shimName)
set_config 'added' $added | Out-Null
}
}
🤖 Prompt for AI Agents
In libexec/scoop-shim.ps1 around lines 77 to 86, the code uses dotted access
($added.$shimName.global) which breaks for property names with special
characters; instead retrieve the property object and read its Value field.
Change the condition to get the property: $prop =
$added.PSObject.Properties[$shimName]; then check if ($prop -and
$prop.Value.global -eq $global) before removing the property and calling
set_config 'added' $added | Out-Null.


# Test if shim is manually added
function Test-ShimAdded($shimName, $gqlobal) {
$added = Get-AddedShimsConfig

# Check if shim exists and matches global flag
return ($added.PSObject.Properties[$shimName] -and $added.$shimName.global -eq $global)
}
Comment on lines +89 to +94
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Fix parameter name typo causing undefined variable reference.

Line 89 declares the parameter as $gqlobal (typo), but line 93 references $global, which is undefined in this function scope. This will cause the function to always return $false or produce incorrect results.

Apply this diff to fix the typo:

-function Test-ShimAdded($shimName, $gqlobal) {
+function Test-ShimAdded($shimName, $global) {
     $added = Get-AddedShimsConfig
 
     # Check if shim exists and matches global flag
     return ($added.PSObject.Properties[$shimName] -and $added.$shimName.global -eq $global)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function Test-ShimAdded($shimName, $gqlobal) {
$added = Get-AddedShimsConfig
# Check if shim exists and matches global flag
return ($added.PSObject.Properties[$shimName] -and $added.$shimName.global -eq $global)
}
function Test-ShimAdded($shimName, $global) {
$added = Get-AddedShimsConfig
# Check if shim exists and matches global flag
return ($added.PSObject.Properties[$shimName] -and $added.$shimName.global -eq $global)
}
🤖 Prompt for AI Agents
In libexec/scoop-shim.ps1 around lines 89 to 94, the function Test-ShimAdded
declares the parameter as $gqlobal but later checks $global, causing an
undefined variable reference; rename the parameter to $global (or update all
references to match the intended name) so the parameter and its usage are
consistent, then run a quick syntax check to ensure no other references to the
misspelled name remain.


if ($SubCommand -notin @('add', 'rm', 'list', 'info', 'alter')) {
if (!$SubCommand) {
Expand All @@ -47,19 +103,21 @@ if ($SubCommand -notin @('add', 'rm', 'list', 'info', 'alter')) {
exit 1
}

$opt, $other, $err = getopt $Args 'g' 'global'
# Update getopt parsing to support --added option
$opt, $other, $err = getopt $Args 'ga' 'global', 'added'
if ($err) { "scoop shim: $err"; exit 1 }

$global = $opt.g -or $opt.global
$showOnlyAdded = $opt.a -or $opt.added

if ($SubCommand -ne 'list' -and $other.Length -eq 0) {
error "<shim_name> must be specified for subcommand '$SubCommand'"
my_usage
my_usages
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify function name change.

Line 115 calls my_usages (plural), but the help text at the top shows my_usage (singular). Ensure this function exists in the imported modules.


🏁 Script executed:

#!/bin/bash
# Search for my_usage and my_usages function definitions
rg -n "function (my_usage|my_usages)" lib/ supporting/ libexec/

Length of output: 71


Rename function call to match defined helper
In libexec/scoop-shim.ps1 at line 115, the code calls my_usages, but only my_usage (singular) is defined (lib/help.ps1:14). Update the invocation:

-    my_usages
+    my_usage
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
my_usages
my_usage
🤖 Prompt for AI Agents
In libexec/scoop-shim.ps1 around line 115, the script calls my_usages but the
defined helper is my_usage (singular); replace the incorrect call with my_usage
so the correct function is invoked and avoid undefined function errors.

exit 1
}

if (-not (Get-FormatData ScoopShims)) {
Update-FormatData "$PSScriptRoot\..\supporting\formats\ScoopTypes.Format.ps1xml"
Update-FormatData "$PSScriptRoot\..\apps\scoop\current\supporting\formats\ScoopTypes.Format.ps1xml"
}
Comment on lines 119 to 121
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Derive format data path robustly; avoid hardcoded apps\scoop\current.

Fallback to repo-relative supporting path first; only try the apps path if present.

-if (-not (Get-FormatData ScoopShims)) {
-    Update-FormatData "$PSScriptRoot\..\apps\scoop\current\supporting\formats\ScoopTypes.Format.ps1xml"
-}
+if (-not (Get-FormatData ScoopShims)) {
+    $candidates = @(
+        "$PSScriptRoot\..\supporting\formats\ScoopTypes.Format.ps1xml",
+        "$PSScriptRoot\..\apps\scoop\current\supporting\formats\ScoopTypes.Format.ps1xml"
+    )
+    foreach ($p in $candidates) {
+        if (Test-Path -LiteralPath $p) { Update-FormatData $p; break }
+    }
+}
🤖 Prompt for AI Agents
In libexec/scoop-shim.ps1 around lines 119 to 121, the format data path is
hardcoded to ..\apps\scoop\current\supporting\formats\ScoopTypes.Format.ps1xml;
change it to first try a repo-relative supporting path (e.g. resolve
"$PSScriptRoot\..\supporting\formats\ScoopTypes.Format.ps1xml" or similar
relative-to-repo location) and if that file doesn't exist, fall back to the apps
path only if Test-Path confirms it exists; construct both candidate paths with
Join-Path/Resolve-Path based on $PSScriptRoot, check existence with Test-Path,
and call Update-FormatData with the first existing path (or skip/emit a warning
if neither exists).


$localShimDir = shimdir $false
Expand All @@ -77,6 +135,8 @@ function Get-ShimInfo($ShimPath) {
}
$info.IsGlobal = $ShimPath.StartsWith("$globalShimDir")
$info.IsHidden = !((Get-Command -Name $info.Name).Path -eq $info.Path)
# Add "Added" field
$info.IsAdded = Test-ShimAdded $info.Name $info.IsGlobal
[PSCustomObject]$info
}

Expand All @@ -95,7 +155,7 @@ switch ($SubCommand) {
error "<command_path> must be specified for subcommand 'add'"
my_usage
exit 1
}
}
$shimName = $other[0]
$commandPath = $other[1]
if ($other.Length -gt 2) {
Expand All @@ -108,14 +168,16 @@ switch ($SubCommand) {
$exCommand = Get-Command $shortPath -ErrorAction SilentlyContinue
if ($exCommand -and $exCommand.CommandType -eq 'Application') {
$commandPath = $exCommand.Path
} # TODO - add support for more command types: Alias, Cmdlet, ExternalScript, Filter, Function, Script, and Workflow
}
}
}
if ($commandPath -and (Test-Path $commandPath)) {
Write-Host "Adding $(if ($global) { 'global' } else { 'local' }) shim " -NoNewline
Write-Host $shimName -ForegroundColor Cyan -NoNewline
Write-Host '...'
shim $commandPath $global $shimName $commandArgs
# Save shim to config
Add-ShimToConfig $shimName $global $commandPath $commandArgs
} else {
Write-Host "ERROR: Command path does not exist: " -ForegroundColor Red -NoNewline
Write-Host $($other[1]) -ForegroundColor Cyan
Expand All @@ -127,6 +189,8 @@ switch ($SubCommand) {
$other | ForEach-Object {
if (Get-ShimPath $_ $global) {
rm_shim $_ (shimdir $global)
# Remove shim from config
Remove-ShimFromConfig $_ $global
} else {
$failed += $_
}
Expand All @@ -140,9 +204,11 @@ switch ($SubCommand) {
}
}
'list' {
$other = @($other) -ne '*'
# Validate all given patterns before matching.
$other | ForEach-Object {
# Handle pattern matching
$patterns = @($other) -ne '*'

# Validate regex patterns
$patterns | ForEach-Object {
try {
$pattern = $_
[void][Regex]::New($pattern)
Expand All @@ -152,19 +218,48 @@ switch ($SubCommand) {
exit 1
}
}
$pattern = $other -join '|'
$shims = @()
if (!$global) {
$shims += Get-ChildItem -Path $localShimDir -Recurse -Include '*.shim', '*.ps1' |
Where-Object { !$pattern -or ($_.BaseName -match $pattern) } |
Select-Object -ExpandProperty FullName
}
if (Test-Path $globalShimDir) {
$shims += Get-ChildItem -Path $globalShimDir -Recurse -Include '*.shim', '*.ps1' |
Where-Object { !$pattern -or ($_.BaseName -match $pattern) } |
Select-Object -ExpandProperty FullName

$pattern = $patterns -join '|'
$shimInfos = @()

if ($showOnlyAdded) {
# When --added option is used, read from config directly
$added = Get-AddedShimsConfig

# Iterate over each shim in config
$added.PSObject.Properties | ForEach-Object {
$shimName = $_.Name
$shimConfig = $_.Value

# Apply regex filter
if (!$pattern -or ($shimName -match $pattern)) {
# Determine shim path based on global flag
$shimPath = Get-ShimPath $shimName $shimConfig.global

if ($shimPath) {
$shimInfos += Get-ShimInfo $shimPath
}
}
}
} else {
Comment on lines +225 to +244
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Honor -g with --added; current code lists both scopes.

Mirror the non---added behavior: without -g, show both; with -g, show only global.

-                # Apply regex filter
-                if (!$pattern -or ($shimName -match $pattern)) {
-                    # Determine shim path based on global flag
-                    $shimPath = Get-ShimPath $shimName $shimConfig.global
-                    if ($shimPath) {
-                        $shimInfos += Get-ShimInfo $shimPath
-                    }
-                }
+                # Apply regex filter and optional -g scope filter
+                if ((-not $pattern -or ($shimName -match $pattern)) -and ((-not $global) -or $shimConfig.global)) {
+                    $shimPath = Get-ShimPath $shimName $shimConfig.global
+                    if ($shimPath) { $shimInfos += Get-ShimInfo $shimPath }
+                }
🤖 Prompt for AI Agents
In libexec/scoop-shim.ps1 around lines 225 to 244, the --added branch currently
ignores the -g/$global switch and always uses the shim's configured scope;
adjust the loop to honor -g by filtering added entries: if $global is set only
include shims whose $shimConfig.global is true, otherwise include all added
shims (no scope filter). Implement this by checking the $global switch inside
the ForEach-Object and skipping entries that are not global when $global is
present, then continue to call Get-ShimPath/Get-ShimInfo as before.

# Original logic: scan file system
$shims = @()

if (!$global) {
$shims += Get-ChildItem -Path $localShimDir -Recurse -Include '*.shim', '*.ps1' |
Where-Object { !$pattern -or ($_.BaseName -match $pattern) } |
Select-Object -ExpandProperty FullName
}
if (Test-Path $globalShimDir) {
$shims += Get-ChildItem -Path $globalShimDir -Recurse -Include '*.shim', '*.ps1' |
Where-Object { !$pattern -or ($_.BaseName -match $pattern) } |
Select-Object -ExpandProperty FullName
}

$shimInfos = $shims.ForEach({ Get-ShimInfo $_ })
}
$shims.ForEach({ Get-ShimInfo $_ }) | Add-Member -TypeName 'ScoopShims' -PassThru

$shimInfos | Format-Table -Property * -AutoSize
}
'info' {
$shimName = $other[0]
Expand Down Expand Up @@ -232,7 +327,7 @@ switch ($SubCommand) {
Write-Host "run 'scoop shim alter $shimName$(if (!$global) { ' --global' })' to alternate its source"
exit 2
}
exit 3
exit
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Return a consistent non-zero exit code on error path.

Use exit 3 (as above) instead of bare exit.

-            exit
+            exit 3
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
exit
exit 3
🤖 Prompt for AI Agents
In libexec/scoop-shim.ps1 around line 330, the error path uses a bare "exit"
which returns a zero/ambiguous status; change it to "exit 3" to return a
consistent non-zero exit code (matching other error paths). Replace the bare
exit statement with exit 3 so callers can reliably detect failure.

}
}
}
Expand Down