Skip to content

Instantly share code, notes, and snippets.

@KirkMunro
Last active February 19, 2016 19:47
Show Gist options
  • Save KirkMunro/a93d78d8100c6e4de150 to your computer and use it in GitHub Desktop.
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)
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