Reputation: 4070
I have found the following behaviour that I do not understand. I have some functions in my $profile
(specifically, that change my prompt
, so function prmopt { }
) with settings that change my prompt and when I start a console, if I dotsource the function ( . PromptCustom
), it takes full effect and the new prompt takes over. However, I don't want my $profile
to be too big so I moved my five or so different prompts into a Module, but when I try to dotsource any of them, nothing happens. They just output what the prompt might look like but do not take over as the default prompt
.
The objective is to be able to have multiple functions that switch between prompts as required (i.e. not a single prompt that applies to every console, for which I would just put function prompt
in my $profile
). When I move functions that follow the below template to a Module, they all break and so I was wondering if that was a scoping issue, and how to achieve the goal of having mutltiple prompt functions in a Module that I can switch between instead of being forced to keep them in my $profile
? (Edit: updating this question as @mklement0 pointed out, since really it's about the required objective i.e. having prompts that I can switch between).
Here is one of my prompt functions that dotsources and takes over as the default prompt perfectly if this function is defined in my $profile
but does nothing if it is put into a Module:
function PromptShortenPath {
# https://stackoverflow.com/questions/1338453/custom-powershell-prompts
function shorten-path([string] $path) {
$loc = $path.Replace($HOME, '~')
# remove prefix for UNC paths
$loc = $loc -replace '^[^:]+::', ''
# make path shorter like tabs in Vim,
# handle paths starting with \\ and . correctly
return ($loc -replace '\\(\.?)([^\\])[^\\]*(?=\\)','\$1$2')
}
function prompt {
# our theme
$cdelim = [ConsoleColor]::DarkCyan
$chost = [ConsoleColor]::Green
$cloc = [ConsoleColor]::Cyan
write-host "$([char]0x0A7) " -n -f $cloc
write-host ([net.dns]::GetHostName()) -n -f $chost
write-host ' {' -n -f $cdelim
write-host (shorten-path (pwd).Path) -n -f $cloc
write-host '}' -n -f $cdelim
return ' '
}
if ($MyInvocation.InvocationName -eq "PromptShortenPath") {
"`nWarning: Must dotsource '$($MyInvocation.MyCommand)' or it will not be applied to this session.`n`n . $($MyInvocation.MyCommand)`n"
} else {
. prompt
}
}
Upvotes: 2
Views: 293
Reputation: 4070
I've finally come down to the following solution. Thanks for helping with this @mklement / @Scepticalist. In the end, I really only needed the global:
invocation. I didn't want a dynamic function (though interesting to see that, will probably be useful) and I did not want the prompt to activate upon importing the module (this was explicitly what I wanted to avoid in fact!).
All of these now work by dropping into any personal Module. Importing the Module will not activate the prompt (this was my desired outcome). Each prompt can then be activated on demand simply by invoking the function that sets that prompt (or its alias).
Edit: Please feel free to add any more prompt functions that do interesting things. I'm always very interested in seeing more useful tricks and variations for prompt configurations! :)
function PromptDefault {
# get-help about_Prompt
# https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_prompts?view=powershell-7
function global:prompt {
"PS $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) ";
# .Link
# https://go.microsoft.com/fwlink/?LinkID=225750
# .ExternalHelp System.Management.Automation.dll-help.xml
$Elevated = ""
$user = [Security.Principal.WindowsIdentity]::GetCurrent();
if ((New-Object Security.Principal.WindowsPrincipal $user).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)) {$Elevated = "Administrator: "}
# $TitleVer = "PS v$($PSVersionTable.PSversion.major).$($PSVersionTable.PSversion.minor)"
$TitleVer = "PowerShell"
$Host.UI.RawUI.WindowTitle = "$($Elevated)$($TitleVer)"
}
}
# More simple alternative prompt, need to dotsource this
function PromptTimeUptime {
function global:prompt {
# Adds date/time to prompt and uptime to title bar
$Elevated = "" ; if (Test-Admin) {$Elevated = "Administrator: "}
$up = Uptime
$Host.UI.RawUI.WindowTitle = $Elevated + "PowerShell [Uptime: $up]" # Title bar info
$path = Get-Location
Write-Host '[' -NoNewline
Write-Host (Get-Date -UFormat '%T') -ForegroundColor Green -NoNewline # $TitleDate = Get-Date -format "dd/MM/yyyy HH:mm:ss"
Write-Host '] ' -NoNewline
Write-Host "$path" -NoNewline
return "> " # Must have a line like this at end of prompt or you always get " PS>" on the prompt
}
}
function PromptTruncatedPaths {
# https://www.johndcook.com/blog/2008/05/12/customizing-the-powershell-command-prompt/
function global:prompt {
$cwd = (get-location).Path
[array]$cwdt=$()
$cwdi = -1
do {$cwdi = $cwd.indexofany("\", $cwdi+1) ; [array]$cwdt+=$cwdi} until($cwdi -eq -1)
if ($cwdt.count -gt 3) { $cwd = $cwd.substring(0,$cwdt[0]) + ".." + $cwd.substring($cwdt[$cwdt.count-3]) }
$host.UI.RawUI.WindowTitle = "$(hostname) – $env:USERDNSDOMAIN$($env:username)"
$host.UI.Write("Yellow", $host.UI.RawUI.BackGroundColor, "[PS]")
" $cwd> "
}
}
function PromptShortenPath {
# https://stackoverflow.com/questions/1338453/custom-powershell-prompts
function global:shorten-path([string] $path) {
$loc = $path.Replace($HOME, '~')
# remove prefix for UNC paths
$loc = $loc -replace '^[^:]+::', ''
# make path shorter like tabs in Vim,
# handle paths starting with \\ and . correctly
return ($loc -replace '\\(\.?)([^\\])[^\\]*(?=\\)','\$1$2')
}
function global:prompt {
# our theme
$cdelim = [ConsoleColor]::DarkCyan
$chost = [ConsoleColor]::Green
$cloc = [ConsoleColor]::Cyan
write-host "$([char]0x0A7) " -n -f $cloc
write-host ([net.dns]::GetHostName()) -n -f $chost
write-host ' {' -n -f $cdelim
write-host (shorten-path (pwd).Path) -n -f $cloc
write-host '}' -n -f $cdelim
return ' '
}
}
function PromptUserAndExecutionTimer {
function global:prompt {
### Title bar info
$user = [Security.Principal.WindowsIdentity]::GetCurrent();
$Elevated = ""
if ((New-Object Security.Principal.WindowsPrincipal $user).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)) {$Elevated = "Admin: "}
$TitleVer = "PS v$($PSVersionTable.PSversion.major).$($PSVersionTable.PSversion.minor)"
# $($executionContext.SessionState.Path.CurrentLocation.path)
### Custom Uptime without seconds (not really necessary)
# $wmi = gwmi -class Win32_OperatingSystem -computer "."
# $LBTime = $wmi.ConvertToDateTime($wmi.Lastbootuptime)
# [TimeSpan]$uptime = New-TimeSpan $LBTime $(get-date)
# $s = "" ; if ($uptime.Days -ne 1) {$s = "s"}
# $TitleUp = "[Up: $($uptime.days) day$s $($uptime.hours) hr $($uptime.minutes) min]"
$Host.UI.RawUI.WindowTitle = "$($Elevated) $($TitleVer)" # $($TitleUp)"
### History ID
$HistoryId = $MyInvocation.HistoryId
# Uncomment below for leading zeros
# $HistoryId = '{0:d4}' -f $MyInvocation.HistoryId
Write-Host -Object "$HistoryId " -NoNewline -ForegroundColor Cyan
### Time calculation
$Success = $?
$LastExecutionTimeSpan = if (@(Get-History).Count -gt 0) {
Get-History | Select-Object -Last 1 | ForEach-Object {
New-TimeSpan -Start $_.StartExecutionTime -End $_.EndExecutionTime
}
}
else {
New-TimeSpan
}
$LastExecutionShortTime = if ($LastExecutionTimeSpan.Days -gt 0) {
"$($LastExecutionTimeSpan.Days + [Math]::Round($LastExecutionTimeSpan.Hours / 24, 2)) d"
}
elseif ($LastExecutionTimeSpan.Hours -gt 0) {
"$($LastExecutionTimeSpan.Hours + [Math]::Round($LastExecutionTimeSpan.Minutes / 60, 2)) h"
}
elseif ($LastExecutionTimeSpan.Minutes -gt 0) {
"$($LastExecutionTimeSpan.Minutes + [Math]::Round($LastExecutionTimeSpan.Seconds / 60, 2)) m"
}
elseif ($LastExecutionTimeSpan.Seconds -gt 0) {
"$($LastExecutionTimeSpan.Seconds + [Math]::Round($LastExecutionTimeSpan.Milliseconds / 1000, 1)) s"
}
elseif ($LastExecutionTimeSpan.Milliseconds -gt 0) {
"$([Math]::Round($LastExecutionTimeSpan.TotalMilliseconds, 0)) ms"
# ms are 1/1000 of a sec so no point in extra decimal places here
}
else {
"0 s"
}
if ($Success) {
Write-Host -Object "[$LastExecutionShortTime] " -NoNewline -ForegroundColor Green
}
else {
Write-Host -Object "! [$LastExecutionShortTime] " -NoNewline -ForegroundColor Red
}
### User, removed
$IsAdmin = (New-Object Security.Principal.WindowsPrincipal ([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
# Write-Host -Object "$($env:USERNAME)$(if ($IsAdmin){ '[A]' } else { '[U]' }) " -NoNewline -ForegroundColor DarkGreen
# Write-Host -Object "$($env:USERNAME)" -NoNewline -ForegroundColor DarkGreen
# Write-Host -Object " [" -NoNewline
# if ($IsAdmin) { Write-Host -Object 'A' -NoNewline -F Red } else { Write-Host -Object 'U' -NoNewline }
# Write-Host -Object "] " -NoNewline
Write-Host "$($env:USERNAME)" -NoNewline -ForegroundColor DarkGreen
Write-Host "[" -NoNewline
if ($IsAdmin) { Write-Host 'A' -NoNewline -F Red } else { Write-Host -Object 'U' -NoNewline }
Write-Host "] " -NoNewline
# ### Path
# $Drive = $pwd.Drive.Name
# $Pwds = $pwd -split "\\" | Where-Object { -Not [String]::IsNullOrEmpty($_) }
# $PwdPath = if ($Pwds.Count -gt 3) {
# $ParentFolder = Split-Path -Path (Split-Path -Path $pwd -Parent) -Leaf
# $CurrentFolder = Split-Path -Path $pwd -Leaf
# "..\$ParentFolder\$CurrentFolder"
# go # }
# elseif ($Pwds.Count -eq 3) {
# $ParentFolder = Split-Path -Path (Split-Path -Path $pwd -Parent) -Leaf
# $CurrentFolder = Split-Path -Path $pwd -Leaf
# "$ParentFolder\$CurrentFolder"
# }
# elseif ($Pwds.Count -eq 2) {
# Split-Path -Path $pwd -Leaf
# }
# else { "" }
# Write-Host -Object "$Drive`:\$PwdPath" -NoNewline
Write-Host $pwd -NoNewline
return "> "
}
}
function PromptSlightlyBroken {
# https://community.spiceworks.com/topic/1965997-custom-cmd-powershell-prompt
# if ($MyInvocation.InvocationName -eq "PromptOverTheTop") {
# "`nWarning: Must dotsource '$($MyInvocation.MyCommand)' or it will not be applied to this session.`n`n . $($MyInvocation.MyCommand)`n"
# } else {
if ($host.name -eq 'ConsoleHost') {
# fff
$Shell = $Host.UI.RawUI
$Shell.BackgroundColor = "Black"
$Shell.ForegroundColor = "White"
$Shell.CursorSize = 10
}
# $Shell=$Host.UI.RawUI
# $size=$Shell.BufferSize
# $size.width=120
# $size.height=3000
# $Shell.BufferSize=$size
# $size=$Shell.WindowSize
# $size.width=120
# $size.height=30
# $Shell.WindowSize=$size
# $Shell.BackgroundColor="Black"
# $Shell.ForegroundColor="White"
# $Shell.CursorSize=10
# $Shell.WindowTitle="Console PowerShell"
function global:Get-Uptime {
$os = Get-WmiObject win32_operatingsystem
$uptime = (Get-Date) - ($os.ConvertToDateTime($os.lastbootuptime))
$days = $Uptime.Days ; if ($days -eq "1") { $days = "$days day" } else { $days = "$days days"}
$hours = $Uptime.Hours ; if ($hours -eq "1") { $hours = "$hours hr" } else { $hours = "$hours hrs"}
$minutes = $Uptime.Minutes ; if ($minutes -eq "1") { $minutes = "$minutes min" } else { $minutes = "$minutes mins"}
$Display = "$days, $hours, $minutes"
Write-Output $Display
}
function Spaces ($numspaces) { for ($i = 0; $i -lt $numspaces; $i++) { Write-Host " " -NoNewline } }
# $MaximumHistoryCount=1024
$IPAddress = @(Get-WmiObject Win32_NetworkAdapterConfiguration | Where-Object {$_.DefaultIpGateway})[0].IPAddress[0]
$IPGateway = @(Get-WmiObject Win32_NetworkAdapterConfiguration | Where-Object {$_.DefaultIpGateway})[0].DefaultIPGateway[0]
$UserDetails = "$env:UserDomain\$env:UserName (PS-HOME: $HOME)"
$PSExecPolicy = Get-ExecutionPolicy
$PSVersion = "$($PSVersionTable.PSVersion.Major).$($PSVersionTable.PSVersion.Minor) ($PSExecPolicy)"
$ComputerAndLogon = "$($env:COMPUTERNAME)"
$ComputerAndLogonSpaces = 28 - $ComputerAndLogon.Length
Clear
Write-Host "-----------------------------------------------------------------------------------------------------------------------" -ForegroundColor Green
Write-Host "| ComputerName: " -nonewline -ForegroundColor Green; Write-Host $ComputerAndLogon -nonewline -ForegroundColor White ; Spaces $ComputerAndLogonSpaces ; Write-Host "UserName:" -nonewline -ForegroundColor Green ; Write-Host " $UserDetails" -ForegroundColor White
Write-Host "| Logon Server: " -nonewline -ForegroundColor Green; Write-Host $($env:LOGONSERVER)"`t`t`t`t" -nonewline -ForegroundColor White ; Write-Host "IP Address:`t" -nonewline -ForegroundColor Green ; Write-Host "`t$IPAddress ($IPGateway)" -ForegroundColor White
Write-Host "| Uptime: " -nonewline -ForegroundColor Green; Write-Host "$(Get-Uptime)`t" -nonewline -ForegroundColor White; Write-Host "PS Version:`t" -nonewline -ForegroundColor Green ; Write-Host "`t$PSVersion" -ForegroundColor White
Write-Host "-----------------------------------------------------------------------------------------------------------------------" -ForegroundColor Green
# Write-Host "-----------------------------------------------------------------------------------------------------------------------" -ForegroundColor Green
# Write-Host "|`tComputerName:`t" -nonewline -ForegroundColor Green; Write-Host $($env:COMPUTERNAME)"`t`t`t`t" -nonewline -ForegroundColor White ; Write-Host "UserName:`t$UserDetails" -ForegroundColor White
# Write-Host "|`tLogon Server:`t" -nonewline -ForegroundColor Green; Write-Host $($env:LOGONSERVER)"`t`t`t`t" -nonewline -ForegroundColor White ; Write-Host "IP Address:`t$IPAddress ($IPGateway)" -ForegroundColor White
# Write-Host "|`tUptime:`t`t" -nonewline -ForegroundColor Green; Write-Host "$(Get-Uptime)`t" -nonewline -ForegroundColor White; Write-Host "PS Version:`t$PSVersion" -ForegroundColor White
# Write-Host "-----------------------------------------------------------------------------------------------------------------------" -ForegroundColor Green
function global:admin {
$Elevated = ""
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal( [Security.Principal.WindowsIdentity]::GetCurrent() )
if ($currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) -eq $true) { $Elevated = "Administrator: " }
$Host.UI.RawUI.WindowTitle = "$Elevated$TitleVer"
}
admin
Set-Location C:\
function global:prompt{
$br = "`n"
Write-Host "[" -noNewLine
Write-Host $(Get-date) -ForegroundColor Green -noNewLine
Write-Host "] " -noNewLine
Write-Host "[" -noNewLine
Write-Host "$env:username" -Foregroundcolor Red -noNewLine
Write-Host "] " -noNewLine
Write-Host "[" -noNewLine
Write-Host $($(Get-Location).Path.replace($home,"~")) -ForegroundColor Yellow -noNewLine
Write-Host $(if ($nestedpromptlevel -ge 1) { '>>' }) -noNewLine
Write-Host "] "
return "> "
}
}
Set-Alias p0 PromptDefault
Set-Alias p-default PromptDefault
Set-Alias p-timer PromptUserAndExecutionTimer # Using this as my console default
Set-Alias p-short PromptShortenPath
Set-Alias p-trunc PromptTruncatedPaths
Set-Alias p-uptime PromptTimeUptime
Set-Alias p-broken PromptSlightlyBroken
# View current prompt with: (get-item function:prompt).scriptblock or cat function:\prompt
Upvotes: 0
Reputation: 439183
Scepticalist's helpful answer provides an effective solution for activating your prompt
function at the time of import.
The approach in your question for activating the function on demand, by later dot-sourcing a function in which the prompt
function is nested, fundamentally cannot work as written if that function is imported from a module, as explained next; for a solution, see the bottom section.
As for what you tried:
. prompt
This doesn't dot-source the definition of function prompt
, it runs the function in the sourcing scope.
Therefore, by nesting the prompt
function definition inside the PromptShortenPath
function, dot-sourcing that defines the prompt
function in the caller's scope automatically, along with the shorten-path
function[1]
If your PromptShortenPath
function is defined outside of a module, dot-sourcing it means that the sourcing scope is the (non-module) caller's current scope, which defines the nested functions there, and with the appearance of a new prompt
function, the interactive prompt string changes, as intended.
By contrast, if your PromptShortenPath
function is defined inside a module, dot-sourcing it means that the sourcing scope is the module of origin, which means that the caller's current scope is unaffected, and never sees the nested shorten-path
and prompt
functions - thus, the interactive prompt string does not change.
By contrast, Scepticalist's solution, by making shorten-path
and prompt
functions top-level functions of the module, implicitly (exports and) imports them both into the caller's scope with Import-Module
, and, again, the appearance of a new prompt
function in the caller's scope changes the interactive prompt string, albeit at the time of importing.
The simplest solution is to define the nested function with scope specifier global:
, which directly defines it in the global scope, irrespective of what scope contains the definition.
As a beneficial side effect, you then no longer have to dot-source the prompt-activating function on invocation.
Note that the solution below embeds helper function shorten-path
in the global:prompt
function to ensure its availability to the latter; the alternative would be to define shorten-path
as global:shorten-path
too, but there is no need to clutter the global scope with helper functions, especially given that name collisions can occur.
# Use a dynamic module to simulate importing the `Set-Prompt` function
# from a (regular, persisted) module.
$null = New-Module {
function Set-Prompt {
# Note the `global:` prefix.
Function global:prompt {
# Note the *embedded* definition of helper function shorten-path,
# which makes it available to the enclosing function only and avoids
# the need to make the helper function global too.
Function shorten-path([string] $path) {
$loc = $path.Replace($HOME, '~')
# remove prefix for UNC paths
$loc = $loc -replace '^[^:]+::', ''
# make path shorter like tabs in Vim,
# handle paths starting with \\ and . correctly
return ($loc -replace '\\(\.?)([^\\])[^\\]*(?=\\)', '\$1$2')
}
# our theme
$cdelim = [ConsoleColor]::DarkCyan
$chost = [ConsoleColor]::Green
$cloc = [ConsoleColor]::Cyan
Write-Host "$([char]0x0A7) " -n -f $cloc
Write-Host ([net.dns]::GetHostName()) -n -f $chost
Write-Host ' {' -n -f $cdelim
Write-Host (shorten-path (pwd).Path) -n -f $cloc
Write-Host '}' -n -f $cdelim
return ' '
}
}
}
# Now that Set-Prompt is imported, invoke it as you would
# any function, and the embedded `prompt` function will take effect.
Set-Prompt
[1] Note that while shorten-path
follows PowerShell's noun-verb naming convention in principle, shorten
is not on the list of approved verbs.
Upvotes: 2
Reputation: 3923
If you remove the outer function and save as modulename.psm1 in a folder by the same name within a module path:
Function shorten-path([string] $path) {
$loc = $path.Replace($HOME, '~')
# remove prefix for UNC paths
$loc = $loc -replace '^[^:]+::', ''
# make path shorter like tabs in Vim,
# handle paths starting with \\ and . correctly
return ($loc -replace '\\(\.?)([^\\])[^\\]*(?=\\)','\$1$2')
}
Function prompt {
# our theme
$cdelim = [ConsoleColor]::DarkCyan
$chost = [ConsoleColor]::Green
$cloc = [ConsoleColor]::Cyan
write-host "$([char]0x0A7) " -n -f $cloc
write-host ([net.dns]::GetHostName()) -n -f $chost
write-host ' {' -n -f $cdelim
write-host (shorten-path (pwd).Path) -n -f $cloc
write-host '}' -n -f $cdelim
return ' '
}
Now just:
Import-Module modulename
Note that the new prompt now takes effect upon importing the function
Upvotes: 2