PowerShell 在多个提示功能和范围界定之间切换

PowerShell switch between multiple prompt functions and scoping

我发现了以下我不理解的行为。我的 $profile 中有一些函数(具体来说,这会改变我的 prompt,所以 function prmopt { })的设置会改变我的提示,当我启动控制台时,如果我 dotsource 函数( . PromptCustom ),它会完全生效,新提示将接管。但是,我不希望我的 $profile 太大,所以我将我的五个左右不同的提示移到了一个模块中,但是当我尝试 dotsource 中的任何一个时,没有任何反应。他们只是输出提示可能的样子,但 not 接管为默认 prompt.

objective 是为了能够有多个功能,可以根据需要在提示之间切换(即不是一个适用于每个控制台的提示,为此我只是将 function prompt 放在我的$profile)。当我将遵循以下模板的函数移动到一个模块时,它们都会中断,所以我想知道这是否是一个范围问题,以及如何实现在一个模块中拥有多个提示函数的目标,我可以在它们之间切换而不是被被迫将它们保留在我的 $profile 中? (编辑:按照@mklement0 指出的那样更新这个问题,因为它实际上是关于必需的 objective 即提示我可以在两者之间切换)。

这是我的提示函数之一,它点源并接管默认提示 完美 如果此函数已在我的 $profile 中定义,但如果它是则不执行任何操作放入模块:

function PromptShortenPath {
    # 
    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')
    }
    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 
    }
}

如果删除外部函数并在模块路径中以相同名称的文件夹中另存为 modulename.psm1:

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')
}
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 ' '
}

现在只需:

Import-Module modulename

请注意,新提示现在在导入函数后生效

提供了在导入时激活 prompt 功能 的有效解决方案

你问题中激活函数 按需 的方法,通过稍后点源 prompt 函数 嵌套的函数,如果该函数是从 模块 导入的,则根本无法 如所写 工作,如下所述; 解决方案见底部

至于你试过的

. prompt

这不是函数 prompt 定义 的点源,它 运行 源范围内的函数.

  • 实际上,它(毫无意义地)打印(一次,作为输出)提示字符串应该是什么,并使函数局部变量停留在调用者的范围内。

因此,通过嵌套prompt函数定义在PromptShortenPath函数内部,点源that在调用者的范围内自动定义 prompt 函数 ,以及 shorten-path 函数 [1]

  • 如果您的 PromptShortenPath 函数是在模块外 定义的 ,点源意味着 作用域是(非模块)调用者的当前作用域,它定义了那里的嵌套函数,并且随着新的prompt函数的出现,交互式提示字符串按预期更改。

  • 相比之下,如果您的 PromptShortenPath 函数定义在 模块内部 ,点源表示源范围是 源模块,这意味着调用者的当前范围未受影响,永远不会看到嵌套的shorten-pathprompt 函数 - 因此,交互式提示字符串 不会 改变。

    • 这值得重复:点源 function(与 script 相对)在来源域而不是调用者的当前范围;也就是说,点源模块中的函数总是在 该模块的 当前作用域中运行它,这与 调用者的 作用域不同且无关(除非调用者恰好是同一模块内的顶级作用域)。

相比之下,怀疑论者的解决方案通过使 shorten-pathprompt 函数成为模块的 顶级 函数,隐式(导出和)导入它们两者都使用 Import-Module 进入调用者的范围,并且调用者范围中新 prompt 函数的出现再次改变了交互式提示字符串,尽管 在导入时 .


也适用于模块的替代方法:

最简单的解决方案是使用范围说明符 global: 定义嵌套函数,它直接在全局范围内定义它,而不考虑包含定义的范围。

作为一个有益的副作用,您不再需要在调用时点源提示激活函数。

注意下面的解决方案global:prompt函数中嵌入了辅助函数shorten-path以确保其对后者的可用性;另一种方法是也将 shorten-path 定义为 global:shorten-path,但是没有必要用辅助函数来混淆全局范围,尤其是考虑到可能会发生名称冲突。

# 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')
      }

      # 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] 请注意,虽然 shorten-path 原则上遵循 PowerShell 的名词-动词命名约定,但 shorten 不在 approved verbs 的列表中。

我最终得出以下解决方案。感谢您帮助解决这个@mklement / @Scepticalist。最后,我真的只需要 global: 调用。我不想要一个动态函数(虽然看起来很有趣,但可能会有用)并且我不希望在导入模块时激活提示(这显然是我实际上想要避免的!)。

所有这些现在都可以通过放入任何个人模块来实现。导入模块 不会 激活提示(这是我想要的结果)。然后,只需调用设置该提示(或其别名)的函数,即可按需激活每个提示。

编辑: 请随时添加更多提示功能来做有趣的事情。我总是很想看到更多有用的提示配置技巧和变体! :)

function PromptDefault {
    # get-help about_Prompt
    # https://docs.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 {
    # 
    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')
    }
    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