Last active
February 19, 2016 19:47
-
-
Save KirkMunro/a93d78d8100c6e4de150 to your computer and use it in GitHub Desktop.
A prototype module that adds $env:PSScriptPath support to Windows PowerShell so that scripts can be invoked by name in well known locations, but only after the command name resolver looks for commands with the same name in modules first (i.e. modules have higher priority than scripts in this solution)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
New-Module -Name BetterScriptDiscovery -ScriptBlock { | |
# First, let's make sure we have a PSScriptPath environment variable that initializes | |
# the same way that the PSModulePath environment variable does | |
$currentUserScriptsPath = [System.IO.Path]::Combine([System.Environment]::GetFolderPath('MyDocuments', [System.Environment+SpecialFolderOption]::DoNotVerify), 'WindowsPowerShell', 'Scripts') | |
$allUserScriptsPath = [System.IO.Path]::Combine([System.Environment]::GetFolderPath('ProgramFiles', [System.Environment+SpecialFolderOption]::DoNotVerify), 'WindowsPowerShell', 'Scripts') | |
$userPsScriptPath = [System.Environment]::GetEnvironmentVariable('PSScriptPath', [System.EnvironmentVariableTarget]::User) | |
$processPsScriptPath = [System.Environment]::GetEnvironmentVariable('PSScriptPath') | |
if ($processPsScriptPath -eq $null) { | |
$processPsScriptPath = '' | |
} | |
$psScriptPathValues = $processPsScriptPath.Split(';', [System.StringSplitOptions]::RemoveEmptyEntries).ForEach{$_.Trim()} | |
if (-not $psScriptPathValues.Contains($allUserScriptsPath)) { | |
$psScriptPathValues.Insert(0, $allUserScriptsPath) | |
} | |
if ([System.String]::IsNullOrEmpty($userPsScriptPath) -and -not $psScriptPathValues.Contains($currentUserScriptsPath)) { | |
$psScriptPathValues.Insert(0, $currentUserScriptsPath) | |
} | |
[System.Environment]::SetEnvironmentVariable('PSScriptPath', ($psScriptPathValues -join ';'), [System.EnvironmentVariableTarget]::Process) | |
# Now, let's set up an event handler that finds commands that normally would not be found | |
$oldCommandNotFoundAction = $ExecutionContext.InvokeCommand.CommandNotFoundAction | |
$ExecutionContext.InvokeCommand.CommandNotFoundAction = { | |
param( | |
$CommandName, | |
$CommandLookupEventArgs | |
) | |
# Pull the entire command string off of the call stack | |
$callStack = @([System.Management.Automation.Runspaces.Runspace]::DefaultRunspace.Debugger.GetCallStack()) | |
$commandText = $callStack[1].Position.Text | |
# Fix the command name if PowerShell decided to prefix it with Get- as part of the command search | |
if (($CommandName -match '^Get-') -and | |
($commandText.IndexOf($CommandName) -eq -1)) { | |
$CommandName = $CommandName -replace '^Get-' | |
} | |
# Add a .ps1 suffix if it is not there already | |
if (-not $CommandName.EndsWith('.ps1')) { | |
$CommandName += '.ps1' | |
} | |
# Now look up the command in PSScriptPath, because we know it's not in $env:Path or discoverable by | |
# PowerShell at this point | |
foreach ($path in @($env:PSScriptPath -split ';')) { | |
$scriptPath = [System.IO.Path]::Combine($path, $CommandName) | |
if ([System.IO.File]::Exists($scriptPath) -and ($scriptCommand = $ExecutionContext.InvokeCommand.GetCommand($scriptPath, 'ExternalScript'))) { | |
# If we found the command, return it to PowerShell using the event arguments and let | |
# PowerShell know that it can stop the search | |
$CommandLookupEventArgs.Command = $scriptCommand | |
$CommandLookupEventArgs.StopSearch = $true | |
break | |
} | |
} | |
} | |
$ExecutionContext.SessionState.Module.OnRemove = { | |
# On unload, reset the PSScriptPath environment variable state to whatever it was before the module | |
# was loaded | |
if ([System.String]::IsNullOrEmpty($processPsScriptPath)) { | |
[System.Environment]::SetEnvironmentVariable('PSScriptPath',$null) | |
} else { | |
[System.Environment]::SetEnvironmentVariable('PSScriptPath',$processPsScriptPath) | |
} | |
# And lastly, reset the CommandNotFoundAction | |
$ExecutionContext.InvokeCommand.CommandNotFoundAction = $oldCommandNotFoundAction | |
} | |
} | Import-Module | |
# Now that we've set that up, let's make sure it works | |
# First, we create a script | |
$script = @' | |
[CmdletBinding()] | |
param( | |
[Parameter(Position=0, ValueFromPipeline=$true)] | |
[ValidateNotNullOrEmpty()] | |
[System.String] | |
$Target = 'world' | |
) | |
process { | |
"Hello, ${Target}!" | |
} | |
'@ | |
$currentUserScriptsPath = Join-Path -Path ([System.Environment]::GetFolderPath('MyDocuments', [System.Environment+SpecialFolderOption]::DoNotVerify)) -ChildPath 'WindowsPowerShell\Scripts' | |
if (-not (Test-Path -LiteralPath $currentUserScriptsPath)) { | |
New-Item -Path $currentUserScriptsPath -ItemType Folder > $null | |
} | |
$scriptPath = Join-Path -Path $currentUserScriptsPath -ChildPath 'Send-WelcomeMessage.ps1' | |
[System.IO.File]::WriteAllText($scriptPath, $script, [System.Text.Encoding]::UTF8) | |
# Then we change our location to one other than where we created our script | |
Set-Location -LiteralPath "${env:SystemDrive}\" | |
# We remove our scripts path from $env:Path in this process | |
$env:Path = @($env:Path -split ';').Where{$_ -ne $currentUserScriptsPath} -join ';' | |
# And invoke our script, and it just works | |
Send-WelcomeMessage $env:USERNAME | |
# Of course if we remove our module, it cleans up after itself | |
Remove-Module BetterScriptDiscovery | |
# And now our script is hidden again | |
Send-WelcomeMessage 'is anyone there!?' |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment