Улучшаем взаимодействие с кавычками и скобками при помощи PSReadline

В этой статье мы продолжим исследование содержимого файла SamplePSReadLineProfile.ps1 из модуля PSReadline.

Во многих редакторах кода взаимодействие со скобками — () {} [] — и кавычками — "" '' — построено следующим образом — вы вводите первую из них, и вторая добавляется автоматически, если тут же при помощи клавиши Backspace вы стираете первую, то вторая также удаляется. Закончив ввод содержимого, вы вводите правую скобку или кавычку, и редактор кода не добавляет еще одну, а перешагивает через существующую.

То же самое мы можем сделать и в консоли PowerShell.

Заглянув в файл SamplePSReadLineProfile.ps1 вы увидите множество полезных функций. Мы же здесь выберем только те, что касаются нашей сегодняшней темы.

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

Code

Для начала нам понадобятся следующие строки.

using namespace System.Management.Automation
using namespace System.Management.Automation.Language

Нужны они нам потому, что нижеприведенные функции ссылаются на классы .net по их имени, без упоминания всего пространства имен.

SmartInsertQuote

Добавим следующий код.

Set-PSReadLineKeyHandler -Key '"',"'" `
                         -BriefDescription SmartInsertQuote `
                         -LongDescription "Insert paired quotes if not already on a quote" `
                         -ScriptBlock {
    param($key, $arg)
​
    $quote = $key.KeyChar
​
    $selectionStart = $null
    $selectionLength = $null
    [Microsoft.PowerShell.PSConsoleReadLine]::GetSelectionState([ref]$selectionStart, [ref]$selectionLength)
​
    $line = $null
    $cursor = $null
    [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)
​
    # If text is selected, just quote it without any smarts
    if ($selectionStart -ne -1)
    {
        [Microsoft.PowerShell.PSConsoleReadLine]::Replace($selectionStart, $selectionLength, $quote + $line.SubString($selectionStart, $selectionLength) + $quote)
        [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($selectionStart + $selectionLength + 2)
        return
    }
​
    $ast = $null
    $tokens = $null
    $parseErrors = $null
    [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$ast, [ref]$tokens, [ref]$parseErrors, [ref]$null)
​
    function FindToken
    {
        param($tokens, $cursor)
​
        foreach ($token in $tokens)
        {
            if ($cursor -lt $token.Extent.StartOffset) { continue }
            if ($cursor -lt $token.Extent.EndOffset) {
                $result = $token
                $token = $token -as [StringExpandableToken]
                if ($token) {
                    $nested = FindToken $token.NestedTokens $cursor
                    if ($nested) { $result = $nested }
                }
​
                return $result
            }
        }
        return $null
    }
​
    $token = FindToken $tokens $cursor
​
    # If we're on or inside a **quoted** string token (so not generic), we need to be smarter
    if ($token -is [StringToken] -and $token.Kind -ne [TokenKind]::Generic) {
        # If we're at the start of the string, assume we're inserting a new string
        if ($token.Extent.StartOffset -eq $cursor) {
            [Microsoft.PowerShell.PSConsoleReadLine]::Insert("$quote$quote ")
            [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($cursor + 1)
            return
        }
​
        # If we're at the end of the string, move over the closing quote if present.
        if ($token.Extent.EndOffset -eq ($cursor + 1) -and $line[$cursor] -eq $quote) {
            [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($cursor + 1)
            return
        }
    }
​
    if ($null -eq $token -or
        $token.Kind -eq [TokenKind]::RParen -or $token.Kind -eq [TokenKind]::RCurly -or $token.Kind -eq [TokenKind]::RBracket) {
        if ($line[0..$cursor].Where{$_ -eq $quote}.Count % 2 -eq 1) {
            # Odd number of quotes before the cursor, insert a single quote
            [Microsoft.PowerShell.PSConsoleReadLine]::Insert($quote)
        }
        else {
            # Insert matching quotes, move cursor to be in between the quotes
            [Microsoft.PowerShell.PSConsoleReadLine]::Insert("$quote$quote")
            [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($cursor + 1)
        }
        return
    }
​
    # If cursor is at the start of a token, enclose it in quotes.
    if ($token.Extent.StartOffset -eq $cursor) {
        if ($token.Kind -eq [TokenKind]::Generic -or $token.Kind -eq [TokenKind]::Identifier -or 
            $token.Kind -eq [TokenKind]::Variable -or $token.TokenFlags.hasFlag([TokenFlags]::Keyword)) {
            $end = $token.Extent.EndOffset
            $len = $end - $cursor
            [Microsoft.PowerShell.PSConsoleReadLine]::Replace($cursor, $len, $quote + $line.SubString($cursor, $len) + $quote)
            [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($end + 2)
            return
        }
    }
​
    # We failed to be smart, so just insert a single quote
    [Microsoft.PowerShell.PSConsoleReadLine]::Insert($quote)
}

Он реализует следующие варианты взаимодействия с двойными и одинарными кавычками.

Если мы вводим кавычку, то вторая добавляется автоматически, курсор при этом остается между кавычками.

"|"

Если мы введем еще одну кавычку, то курсор просто перешагнет через кавычку, добавленную на предыдущем шаге.

""|

Если же курсор находится не перед второй кавычкой, как в предыдущем примере, а перед первой, и мы вводим еще один символ кавычки, то подразумевается, что мы хотим добавить еще пару кавычек перед уже введенными.

|"" -> "|" ""

Кроме того, если слева от курсора уже есть некоторое количество кавычек, то действия при вводе очередной кавычки зависят от их парности.

Если слева от курсора находится нечетное количество кавычек, то нажав на клавиши ввода кавычки мы добавим не две кавычки, а одну, закрывающую некую непарную кавычку слева.

"left "inner" right| -> "left "inner" right"|

Если же слева от курсора находится четное число кавычек, то, как и в первом примере, будет добавлена пара кавычек с расположенным между ними курсором.

"left "inner" right" | -> "left "inner" right" "|"

Также, если курсор стоит непосредственно перед неким токеном — командой, переменной, ключевым словом или набором символов, то нажатие на клавиши ввода кавычки приведет к тому, что весь этот токен будет заключен в кавычки.

|someword -> "someword"|

А если перед вводом символа кавычки мы выделим некую часть вводимой команды, то в данном случае в кавычки будет заключена вся выделенная часть.

Some phrase with selected part. -> Some phrase with "selected part".

Если же кавычка вводится в ситуации, не соответствующей ни одной из вышеприведенных, то, как и в обычном случае, добавляется один символ кавычки.

InsertPairedBraces

Далее нам потребуется следующая часть кода.

Set-PSReadLineKeyHandler -Key '(','{','[' `
                         -BriefDescription InsertPairedBraces `
                         -LongDescription "Insert matching braces" `
                         -ScriptBlock {
    param($key, $arg)
​
    $closeChar = switch ($key.KeyChar)
    {
         '(' { [char]')'; break }
         '{' { [char]'}'; break }
         '[' { [char]']'; break }
    }
​
    $selectionStart = $null
    $selectionLength = $null
    [Microsoft.PowerShell.PSConsoleReadLine]::GetSelectionState([ref]$selectionStart, [ref]$selectionLength)
​
    $line = $null
    $cursor = $null
    [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)
    
    if ($selectionStart -ne -1)
    {
      # Text is selected, wrap it in brackets
      [Microsoft.PowerShell.PSConsoleReadLine]::Replace($selectionStart, $selectionLength, $key.KeyChar + $line.SubString($selectionStart, $selectionLength) + $closeChar)
      [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($selectionStart + $selectionLength + 2)
    } else {
      # No text is selected, insert a pair
      [Microsoft.PowerShell.PSConsoleReadLine]::Insert("$($key.KeyChar)$closeChar")
      [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($cursor + 1)
    }
}

Его задача состоит в том, чтобы, при вводе открывающей скобки — ( { [ — была также добавлена и закрывающая — ] } ), при этом курсор остается внутри скобок.

(|) [|] {|}

Кроме того, если пере вводом открывающей скобки мы выделили некоторую часть команды, то вся эта часть будет заключена в скобки.

Some phrase with selected part. -> Some phrase with (selected part).

SmartCloseBraces

Переходим к следующей части кода.

Set-PSReadLineKeyHandler -Key ')',']','}' `
                         -BriefDescription SmartCloseBraces `
                         -LongDescription "Insert closing brace or skip" `
                         -ScriptBlock {
    param($key, $arg)
​
    $line = $null
    $cursor = $null
    [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)
​
    if ($line[$cursor] -eq $key.KeyChar)
    {
        [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($cursor + 1)
    }
    else
    {
        [Microsoft.PowerShell.PSConsoleReadLine]::Insert("$($key.KeyChar)")
    }
}

Этот блок кода срабатывает на ввод закрывающих скобок - ) } ].

То есть, если курсор стоит перед закрывающей скобкой, вполне вероятно, автоматически добавленной при вводе открывающей скобки, то нажатие на клавиши символа закрывающей скобки приведет к тому, что курсор через нее просто перешагнет.

(text|) -> (text)|

Если же перед курсором стоит какой-либо другой символ, либо курсор находится в самом конце вводимой команды, то закрывающая скобка будет добавлена.

(text| -> (text)|

SmartBackspace

Далее добавим следующий блок кода.

Set-PSReadLineKeyHandler -Key Backspace `
                         -BriefDescription SmartBackspace `
                         -LongDescription "Delete previous character or matching quotes/parens/braces" `
                         -ScriptBlock {
    param($key, $arg)
​
    $line = $null
    $cursor = $null
    [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)
​
    if ($cursor -gt 0)
    {
        $toMatch = $null
        if ($cursor -lt $line.Length)
        {
            switch ($line[$cursor])
            {
                 '"' { $toMatch = '"'; break }
                 "'" { $toMatch = "'"; break }
                 ')' { $toMatch = '('; break }
                 ']' { $toMatch = '['; break }
                 '}' { $toMatch = '{'; break }
            }
        }
​
        if ($toMatch -ne $null -and $line[$cursor-1] -eq $toMatch)
        {
            [Microsoft.PowerShell.PSConsoleReadLine]::Delete($cursor - 1, 2)
        }
        else
        {
            [Microsoft.PowerShell.PSConsoleReadLine]::BackwardDeleteChar($key, $arg)
        }
    }
}
​

Он определяет поведение клавиши Backspace, а именно - если курсор находится непосредственно между кавычками - "" '' - или скобками - () {} [] — то нажатие Backspace приведет к тому, что удалены будут обе скобки или кавычки.

"|" -> |

{|} -> |

В остальных ситуациях клавиша Backspace работает как обычно.

ParenthesizeSelection

Добавим еще один полезный блок кода.

Set-PSReadLineKeyHandler -Key 'Alt+(' `
                         -BriefDescription ParenthesizeSelection `
                         -LongDescription "Put parenthesis around the selection or entire line and move the cursor to after the closing parenthesis" `
                         -ScriptBlock {
    param($key, $arg)
​
    $selectionStart = $null
    $selectionLength = $null
    [Microsoft.PowerShell.PSConsoleReadLine]::GetSelectionState([ref]$selectionStart, [ref]$selectionLength)
​
    $line = $null
    $cursor = $null
    [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)
    if ($selectionStart -ne -1)
    {
        [Microsoft.PowerShell.PSConsoleReadLine]::Replace($selectionStart, $selectionLength, '(' + $line.SubString($selectionStart, $selectionLength) + ')')
        [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($selectionStart + $selectionLength + 2)
    }
    else
    {
        [Microsoft.PowerShell.PSConsoleReadLine]::Replace(0, $line.Length, '(' + $line + ')')
        [Microsoft.PowerShell.PSConsoleReadLine]::EndOfLine()
    }
}

Его действие заключается в том, что при нажатии на сочетание клавиш Shift+Alt+9 — фактически Alt+( — вся выделенная часть вводимой команды будет заключена в круглые скобки. Нужно сказать, что в этом он повторяет один из вышеприведенных блоков кода.

Some phrase with selected part. -> Some phrase with (selected part).

Чем он отличается, так это тем, что при нажатии этого сочетания клавиш без предварительного выделения некоторой команды, в круглые скобки будет заключена вся введенная команда.

Some command -> (Some command)

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

Get-Process -Name pwsh

Вы нажимаете сочетание клавиш Shift+Alt+9 и командная строка приобретает следующий вид.

(Get-Process -Name pwsh)

Все что нам остается, так это добавить точку и имя нужного свойства.

(Get-Process -Name pwsh).Path

Return

В этой статье мы рассмотрели несколько полезных механизмов, представленных в файле SamplePSReadLineProfile.ps1 модуля PSReadline, которые позволяют сделать работу с консолью PowerShell еще более удобной, путем добавления в нее функциональности, доступной до этого преимущественно в редакторах кода.

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход /  Изменить )

Google photo

Для комментария используется ваша учётная запись Google. Выход /  Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход /  Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход /  Изменить )

Connecting to %s