PowerShell在多个提示功能和作用域之间切换

PowerShell在多个提示功能和作用域之间切换,powershell,scope,prompt,Powershell,Scope,Prompt,我发现以下行为我不理解。我的$profile中有一些函数专门用来更改我的提示,因此函数prmopt{}的设置会更改我的提示,并且当我启动控制台时,如果我对函数进行了源代码转换的话。PromptCustom,它将完全生效,新的提示将接管。但是,我不希望我的$profile太大,所以我将五个左右不同的提示移到了一个模块中,但是当我尝试对其中任何一个进行dotsource时,什么也没有发生。它们只输出提示的外观,但不作为默认提示 我们的目标是能够根据需要在提示之间切换多个功能,也就是说,没有一个单独的

我发现以下行为我不理解。我的$profile中有一些函数专门用来更改我的提示,因此函数prmopt{}的设置会更改我的提示,并且当我启动控制台时,如果我对函数进行了源代码转换的话。PromptCustom,它将完全生效,新的提示将接管。但是,我不希望我的$profile太大,所以我将五个左右不同的提示移到了一个模块中,但是当我尝试对其中任何一个进行dotsource时,什么也没有发生。它们只输出提示的外观,但不作为默认提示

我们的目标是能够根据需要在提示之间切换多个功能,也就是说,没有一个单独的提示适用于每个控制台,我只会在$profile中添加函数提示。当我将遵循以下模板的函数移动到一个模块中时,它们都会中断,因此我想知道这是否是一个范围问题,以及如何实现在一个模块中具有多个提示函数的目标,我可以在这些函数之间切换,而不是被迫将它们保留在我的$profile中?编辑:正如@mklement0所指出的,更新这个问题,因为它实际上是关于所需的目标,也就是说,有我可以切换的提示

下面是我的一个提示函数,如果在我的$profile中定义了该函数,则该函数完全可以作为默认提示使用,但如果将其放入模块中,则不会执行任何操作:

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

如果删除外部函数并在模块路径中以相同名称另存为文件夹中的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$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 ' '
}
现在只需:

Import-Module modulename
请注意,新的提示现在在导入功能时生效

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

如下文所述,如果函数是从模块导入的,则您问题中所述的按需激活函数的方法,即稍后点源化一个嵌套提示函数的函数,基本上无法像编写的那样工作;有关解决方案,请参见底部部分

至于你尝试了什么:

。提示

这不是函数提示符的定义,而是在寻源范围内运行函数

实际上,它会毫无意义地打印一次,作为输出,提示字符串应该是什么,并使函数局部变量停留在调用方的范围内。 因此,通过将提示函数定义嵌套在PromptShortenPath函数中,可以自动在调用者的作用域中定义提示函数的点源,以及shortenPath函数[1]

如果您的PromptShortenPath函数是在模块外部定义的,则点寻源意味着寻源范围是非模块调用方的当前范围,该范围定义了其中的嵌套函数,并且随着新提示函数的出现,交互式提示字符串将按预期进行更改

相反,如果PromptShortenPath函数是在模块内部定义的,则点源表示源作用域是源模块,这意味着调用方的当前作用域不受影响,并且从未看到嵌套的shorten path和prompt函数-因此,交互式提示字符串不会更改

这需要重复:点源函数与脚本相反,它在源代码的作用域的当前作用域中运行函数,而不是在调用方的当前作用域中运行函数;也就是说,点源函数总是在模块的当前作用域中运行,这与调用方的作用域不同,也与调用方的作用域无关,除非调用方恰好是同一模块中的顶级作用域。 相反,怀疑论者的解决方案是,通过使shorten path和prompt函数成为模块的顶级函数,使用Import module隐式地将它们导出并导入调用者的作用域,并且,调用者作用域中新的prompt函数的出现会改变交互提示字符串,尽管是在进口时

也适用于模块的替代方法: 最简单的解决方案是使用作用域说明符global:定义嵌套函数,该说明符直接在全局作用域中定义它,而不管定义包含在哪个作用域中

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

请注意,下面的解决方案将helper函数shorten path嵌入到global:prompt函数中,以确保后者可用;另一种方法是将shortenpath定义为global:shortenpath,但是没有必要用helper函数来混乱全局范围,特别是在可能发生名称冲突的情况下

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

我最终得出以下解决方案。感谢您帮助这位@mklement/@politicalist。最后,我真的 只需要全局:调用。我不想要一个动态函数,虽然我很感兴趣,但它可能会很有用,而且我不想在导入模块时激活提示,事实上,这正是我想要避免的

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

编辑:请随意添加任何更多的提示功能,做有趣的事情。我总是非常有兴趣看到更多有用的技巧和变化的提示配置!:

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 {
    # 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