PowerShell переключение между несколькими функциями подсказки и областью видимости - PullRequest
2 голосов
/ 12 марта 2020

Я обнаружил следующее поведение, которое я не понимаю. В моем $profile есть некоторые функции (в частности, меняющие мой prompt, поэтому function prmopt { }) с настройками, которые меняют мое приглашение, и когда я запускаю консоль, если я получаю источник функции (. PromptCustom), требуется полный эффект, и новое приглашение вступает во владение. Однако я не хочу, чтобы мои $profile были слишком большими, поэтому я переместил свои пять или около того разных приглашений в модуль, но когда я пытаюсь получить какой-либо из них, ничего не происходит. Они просто выводят, как может выглядеть приглашение, но не принимают значение по умолчанию prompt.

Цель состоит в том, чтобы иметь возможность иметь несколько функций, которые переключаются между запросами по мере необходимости ( то есть ни одна подсказка, которая применяется к каждой консоли, для которой я бы просто вставил function prompt в мою $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 
    }
}

Ответы [ 3 ]

2 голосов
/ 13 марта 2020

Полезный ответ Scepticalist предоставляет эффективное решение для активации вашей prompt функции во время импорта .

Подход в вашем вопросе для активации функции по требованию , путем последующего точечного поиска функции, в которой функция prompt является вложенной , принципиально не может работать как написано , если эта функция импортирована из модуля , как объяснено далее; решение см. В нижнем разделе .

Что касается того, что вы пытались :

. prompt

Это не точечный источник определения функции prompt, он выполняет функцию в области поиска.

  • In эффект, он (бессмысленно) печатает (один раз, как вывод), какой должна быть строка приглашения, и заставляет локальные переменные функции оставаться в области действия вызывающего.

Следовательно, вложенность определение функции prompt внутри функции PromptShortenPath, точечный источник , который определяет функцию prompt в области действия вызывающего абонента автоматически вместе с функцией shorten-path [1]

  • Если ваша функция PromptShortenPath определена вне модуля , точечный источник означает, что источник scope - это (немодульная) текущая область вызова вызывающего абонента , которая определяет вложенные функции там и с появлением нового prompt f. Кроме того, строка интерактивного приглашения изменяется, как и предполагалось.

  • В отличие от этого, если ваша PromptShortenPath функция определена внутри модуля , точечный источник означает, что область поиска - это модуль происхождения , что означает, что текущая область действия *1074* вызывающего абонента не затронута и никогда не видит вложенные функции shorten-path и prompt - таким образом, строка интерактивного приглашения не изменяется.

    • Повторяется: точечный источник функция (в отличие от сценария ) запускает функцию в текущей области действия области происхождения , а не текущей области вызова вызывающей стороны ; то есть точечная выборка функции из модуля неизменно запускает ее в текущей области этого модуля, которая отличается от области действия *1095* вызывающей стороны и не связана с ней (если только вызывающая сторона не является область верхнего уровня внутри того же модуля).

Напротив, решение Scepticalist, сделав функции shorten-path и prompt верхнего уровня функции модуля неявно (экспортирует и) импортируют их как в область вызова с помощью Import-Module, и, опять же, появление новой функции prompt в области вызова изменяет строку интерактивного приглашения, хотя в время импорта .


Альтернативный подход, который также работает с модулями:

Самое простое решение - определить вложенную функцию со спецификатором области действия global:, который непосредственно определяет это в глобальной области видимости, независимо от того, какая область содержит определение.

В качестве полезного побочного эффекта вам больше не нужно ставить точечный источник функция быстрого вызова при вызове.

Обратите внимание, что решение ниже включает вспомогательную функцию shorten-path в функцию global:prompt, чтобы обеспечить ее доступность для последней; альтернативой может быть также определение 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$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 отсутствует в списке утвержденных глаголов .

2 голосов
/ 12 марта 2020

Если вы удалите внешнюю функцию и сохраните ее как 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

Обратите внимание, что новое приглашение вступает в силу после импорта функции

0 голосов
/ 17 марта 2020

Я наконец пришел к следующему решению. Спасибо за помощь с этим @mklement / @Scepticalist. В конце концов, мне действительно нужен был только вызов global:. Я не хотел динамическую c функцию (хотя интересно посмотреть, что, вероятно, будет полезно), и я не хотел, чтобы подсказка активировалась при импорте модуля (это было явно то, чего я действительно хотел избежать!).

Все это теперь работает, опускаясь в любой персональный модуль. Импорт модуля не активирует подсказку (это был мой желаемый результат). Каждая подсказка может быть активирована по требованию, просто вызвав функцию, которая устанавливает эту подсказку (или ее псевдоним).

Редактировать: Пожалуйста, не стесняйтесь добавлять любые другие функции подсказки, которые делают интересные вещи , Мне всегда очень интересно видеть больше полезных трюков и вариантов для быстрой конфигурации! :)

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