Типичный вопрос разработчиков под Windows: «Почему здесь до сих пор нет <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>?». Будь то мощное пролистывание less или привычные инструменты grep или sed, разработчики под Windows хотят получить лёгкий доступ к этим командам в повседневной работе.

Подсистема Windows для Linux (WSL) сделала огромный шаг вперёд в этом отношении. Она позволяет вызывать команды Linux из Windows, проксируя их через wsl.exe (например, wsl ls). Хотя это значительное улучшение, но такой вариант страдает от ряда недостатков.

  • Повсеместное добавление wsl утомительно и неестественно.
  • Пути Windows в аргументах не всегда срабатывают, потому что обратные слэши интерпретируются как escape-символы, а не разделители каталогов.
  • Пути Windows в аргументах не переводятся в соответствующую точку монтирования в WSL.
  • Не учитываются параметры по умолчанию в профилях WSL с алиасами и переменными окружения.
  • Не поддерживается завершение путей Linux.
  • Не поддерживается завершение команд.
  • Не поддерживается завершение аргументов.

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

Оболочки функций PowerShell


C помощью оболочек функций PowerShell мы можем добавить автозавершение команд и устранить необходимость в префиксах wsl, транслируя пути Windows в пути WSL. Основные требования к оболочкам:

  • Для каждой команды Linux должна быть одна оболочка функции с тем же именем.
  • Оболочка должна распознавать пути Windows, переданные в качестве аргументов, и преобразовывать их в пути WSL.
  • Оболочка должна вызывать wsl с соответствующей командой Linux на любой вход конвейера и передавая любые аргументы командной строки, переданные функции.

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

# The commands to import.
$commands = "awk", "emacs", "grep", "head", "less", "ls", "man", "sed", "seq", "ssh", "tail", "vim"
 
# Register a function for each command.
$commands | ForEach-Object { Invoke-Expression @"
Remove-Alias $_ -Force -ErrorAction Ignore
function global:$_() {
    for (`$i = 0; `$i -lt `$args.Count; `$i++) {
        # If a path is absolute with a qualifier (e.g. C:), run it through wslpath to map it to the appropriate mount point.
        if (Split-Path `$args[`$i] -IsAbsolute -ErrorAction Ignore) {
            `$args[`$i] = Format-WslArgument (wsl.exe wslpath (`$args[`$i] -replace "\\", "/"))
        # If a path is relative, the current working directory will be translated to an appropriate mount point, so just format it.
        } elseif (Test-Path `$args[`$i] -ErrorAction Ignore) {
            `$args[`$i] = Format-WslArgument (`$args[`$i] -replace "\\", "/")
        }
    }
 
    if (`$input.MoveNext()) {
        `$input.Reset()
        `$input | wsl.exe $_ (`$args -split ' ')
    } else {
        wsl.exe $_ (`$args -split ' ')
    }
}
"@
}

Список $command определяет команды для импорта. Затем мы динамически генерируем обёртку функции для каждой из них, используя команду Invoke-Expression (сначала удалив любые алиасы, которые будут конфликтовать с функцией).

Функция перебирает аргументы командной строки, определяет пути Windows с помощью команд Split-Path и Test-Path, а затем преобразует эти пути в пути WSL. Мы запускаем пути через вспомогательную функцию Format-WslArgument, которую определим позже. Она экранирует специальные символы, такие как пробелы и скобки, которые в противном случае были бы неверно истолкованы.

Наконец, передаём wsl входные данные конвейера и любые аргументы командной строки.

С помощью таких обёрток можно вызывать любимые команды Linux более естественным способом, не добавляя префикс wsl и не беспокоясь о том, как преобразуются пути:

  • man bash
  • less -i $profile.CurrentUserAllHosts
  • ls -Al C:\Windows\ | less
  • grep -Ein error *.log
  • tail -f *.log

Здесь показан базовый набор команд, но вы можете создать оболочку для любой команды Linux, просто добавив её в список. Если вы добавите этот код в свой профиль PowerShell, эти команды будут доступны вам в каждом сеансе PowerShell, как и нативные команды!

Параметры по умолчанию


В Linux принято определять алиасы и/или переменные окружения в профилях (login profile), задавая параметры по умолчанию для часто используемых команд (например, alias ls=ls -AFh или export LESS=-i). Один из недостатков проксирования через неинтерактивную оболочку wsl.exe — то, что профили не загружаются, поэтому эти параметры по умолчанию недоступны (т. е. ls в WSL и wsl ls будут вести себя по-разному с алиасом, определённым выше).

PowerShell предоставляет $PSDefaultParameterValues, стандартный механизм для определения параметров по умолчанию, но только для командлетов и расширенных функций. Конечно, можно из наших оболочек сделать расширенные функции, но это вносит лишние осложнения (так, PowerShell соотносит частичные имена параметров (например, -a соотносится с -ArgumentList), которые будут конфликтовать с командами Linux, принимающими частичные имена в качестве аргументов), а синтаксис для определения значений по умолчанию будет не самым подходящим (для определения аргументов по умолчанию требуется имя параметра в ключе, а не только имя команды).

Однако с небольшим изменением наших оболочек мы можем внедрить модель, аналогичную $PSDefaultParameterValues, и включить параметры по умолчанию для команд Linux!

function global:$_() {
    …
 
    `$defaultArgs = ((`$WslDefaultParameterValues.$_ -split ' '), "")[`$WslDefaultParameterValues.Disabled -eq `$true]
    if (`$input.MoveNext()) {
        `$input.Reset()
        `$input | wsl.exe $_ `$defaultArgs (`$args -split ' ')
    } else {
        wsl.exe $_ `$defaultArgs (`$args -split ' ')
    }
}

Передавая $WslDefaultParameterValues в командную строку, мы отправляем параметры через wsl.exe. Ниже показано, как добавить инструкции в профиль PowerShell для настройки параметров по умолчанию. Теперь мы можем это сделать!

$WslDefaultParameterValues["grep"] = "-E"
$WslDefaultParameterValues["less"] = "-i"
$WslDefaultParameterValues["ls"] = "-AFh --group-directories-first"

Поскольку параметры моделируются после $PSDefaultParameterValues, вы можете легко их отключить на время, установив ключ "Disabled" в значение $true. Дополнительное преимущество отдельной хэш-таблицы в возможности отключить $WslDefaultParameterValues отдельно от $PSDefaultParameterValues.

Автозавершение аргументов


PowerShell позволяет регистрировать завершители аргументов с помощью команды Register-ArgumentCompleter. В Bash есть мощные программируемые средства для автозавершения. WSL позволяет вызывать bash из PowerShell. Если мы можем зарегистрировать завершители аргументов для наших оболочек функций PowerShell и вызвать bash для создания завершений, то получим полное автозавершение аргументов с той же точностью, что и в самом bash!

# Register an ArgumentCompleter that shims bash's programmable completion.
Register-ArgumentCompleter -CommandName $commands -ScriptBlock {
    param($wordToComplete, $commandAst, $cursorPosition)
 
    # Map the command to the appropriate bash completion function.
    $F = switch ($commandAst.CommandElements[0].Value) {
        {$_ -in "awk", "grep", "head", "less", "ls", "sed", "seq", "tail"} {
            "_longopt"
            break
        }
 
        "man" {
            "_man"
            break
        }
 
        "ssh" {
            "_ssh"
            break
        }
 
        Default {
            "_minimal"
            break
        }
    }
 
    # Populate bash programmable completion variables.
    $COMP_LINE = "`"$commandAst`""
    $COMP_WORDS = "('$($commandAst.CommandElements.Extent.Text -join "' '")')" -replace "''", "'"
    for ($i = 1; $i -lt $commandAst.CommandElements.Count; $i++) {
        $extent = $commandAst.CommandElements[$i].Extent
        if ($cursorPosition -lt $extent.EndColumnNumber) {
            # The cursor is in the middle of a word to complete.
            $previousWord = $commandAst.CommandElements[$i - 1].Extent.Text
            $COMP_CWORD = $i
            break
        } elseif ($cursorPosition -eq $extent.EndColumnNumber) {
            # The cursor is immediately after the current word.
            $previousWord = $extent.Text
            $COMP_CWORD = $i + 1
            break
        } elseif ($cursorPosition -lt $extent.StartColumnNumber) {
            # The cursor is within whitespace between the previous and current words.
            $previousWord = $commandAst.CommandElements[$i - 1].Extent.Text
            $COMP_CWORD = $i
            break
        } elseif ($i -eq $commandAst.CommandElements.Count - 1 -and $cursorPosition -gt $extent.EndColumnNumber) {
            # The cursor is within whitespace at the end of the line.
            $previousWord = $extent.Text
            $COMP_CWORD = $i + 1
            break
        }
    }
 
    # Repopulate bash programmable completion variables for scenarios like '/mnt/c/Program Files'/<TAB> where <TAB> should continue completing the quoted path.
    $currentExtent = $commandAst.CommandElements[$COMP_CWORD].Extent
    $previousExtent = $commandAst.CommandElements[$COMP_CWORD - 1].Extent
    if ($currentExtent.Text -like "/*" -and $currentExtent.StartColumnNumber -eq $previousExtent.EndColumnNumber) {
        $COMP_LINE = $COMP_LINE -replace "$($previousExtent.Text)$($currentExtent.Text)", $wordToComplete
        $COMP_WORDS = $COMP_WORDS -replace "$($previousExtent.Text) '$($currentExtent.Text)'", $wordToComplete
        $previousWord = $commandAst.CommandElements[$COMP_CWORD - 2].Extent.Text
        $COMP_CWORD -= 1
    }
 
    # Build the command to pass to WSL.
    $command = $commandAst.CommandElements[0].Value
    $bashCompletion = ". /usr/share/bash-completion/bash_completion 2> /dev/null"
    $commandCompletion = ". /usr/share/bash-completion/completions/$command 2> /dev/null"
    $COMPINPUT = "COMP_LINE=$COMP_LINE; COMP_WORDS=$COMP_WORDS; COMP_CWORD=$COMP_CWORD; COMP_POINT=$cursorPosition"
    $COMPGEN = "bind `"set completion-ignore-case on`" 2> /dev/null; $F `"$command`" `"$wordToComplete`" `"$previousWord`" 2> /dev/null"
    $COMPREPLY = "IFS=`$'\n'; echo `"`${COMPREPLY[*]}`""
    $commandLine = "$bashCompletion; $commandCompletion; $COMPINPUT; $COMPGEN; $COMPREPLY" -split ' '
 
    # Invoke bash completion and return CompletionResults.
    $previousCompletionText = ""
    (wsl.exe $commandLine) -split '\n' |
    Sort-Object -Unique -CaseSensitive |
    ForEach-Object {
        if ($wordToComplete -match "(.*=).*") {
            $completionText = Format-WslArgument ($Matches[1] + $_) $true
            $listItemText = $_
        } else {
            $completionText = Format-WslArgument $_ $true
            $listItemText = $completionText
        }
 
        if ($completionText -eq $previousCompletionText) {
            # Differentiate completions that differ only by case otherwise PowerShell will view them as duplicate.
            $listItemText += ' '
        }
 
        $previousCompletionText = $completionText
        [System.Management.Automation.CompletionResult]::new($completionText, $listItemText, 'ParameterName', $completionText)
    }
}
 
# Helper function to escape characters in arguments passed to WSL that would otherwise be misinterpreted.
function global:Format-WslArgument([string]$arg, [bool]$interactive) {
    if ($interactive -and $arg.Contains(" ")) {
        return "'$arg'"
    } else {
        return ($arg -replace " ", "\ ") -replace "([()|])", ('\$1', '`$1')[$interactive]
    }
}

Код немного плотный без понимания некоторых внутренних функций bash, но в основном мы делаем следующее:

  • Регистрируем завершатель аргументов для всех наших обёрток функций, передавая список $commands в параметр -CommandName для Register-ArgumentCompleter.
  • Сопоставляем каждую команду с функцией оболочки, которую использует bash для автозавершения (для определения спецификаций автозавершения в bash используется $F, сокращение от complete -F <FUNCTION>).
  • Преобразуем аргументы PowerShell $wordToComplete, $commandAst и $cursorPosition в формат, ожидаемый функциями автозавершения bash в соответствии со спецификациями программируемого автозавершения bash.
  • Составляем командную строку для передачи в wsl.exe, который обеспечивает правильную настройку среды, вызывает соответствующую функцию автозавершения и выводит результаты с разбиением по строкам.
  • Затем вызываем wsl с командной строкой, разделяем выдачу разделителями строк и генерируем для каждой CompletionResults, сортируя их и экранируя символы, такие как пробелы и скобки, которые в противном случае были бы неверно истолкованы.

В итоге наши оболочки команд Linux будут использовать точно такое же автозавершение, как в bash! Например:

  • ssh -c <TAB> -J <TAB> -m <TAB> -O <TAB> -o <TAB> -Q <TAB> -w <TAB> -b <TAB>

Каждое автозавершение подоставляет значения, специфичные для предыдущего аргумента, считывая данные конфигурации, такие как известные хосты, из WSL!

<TAB> будет циклически перебирать параметры. <Ctrl + пробел> покажет все доступные опции.

Кроме того, поскольку теперь у нас работает автозавершение bash, вы можете автозавершать пути Linux непосредственно в PowerShell!

  • less /etc/<TAB>
  • ls /usr/share/<TAB>
  • vim ~/.bash<TAB>

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

Заключение


С помощью PowerShell и WSL мы можем интегрировать команды Linux в Windows как нативные приложения. Нет необходимости искать билды Win32 или утилиты Linux или прерывать рабочий процесс, переходя в Linux-шелл. Просто установите WSL, настройте профиль PowerShell и перечислите команды, которые хотите импортировать! Богатое автозавершение для параметров команд и путей к файлам Linux и Windows — это функциональность, которой сегодня нет даже в нативных командах Windows.

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

Какие команды Linux вы считаете наиболее полезными? Каких ещё привычных вещей не хватает при работе в Windows? Пишите в комментариях или на GitHub!

Комментарии (25)


  1. Bedal
    28.09.2019 14:47

    разработчики под Windows хотят получить лёгкий доступ к этим командам в повседневной работе
    разработчики обычно как раз не хотят. У них полным-полно других, не менее (а то и более) мощных и удобных инструментов. Вот админам, эникейшикам и любителям небольших скриптов для себя — да, другое дело. Заметьте, это вовсе не унижает перечисленные профессии и занятия, просто они другие.


    1. sved
      29.09.2019 04:25
      +1

      Похоже, у вас очень узкое представление о разработчиках, либо у вас очень специфические задачи


      1. Bedal
        29.09.2019 14:17
        -1

        Наверно, Вы правы. 40 лет личного программленья и руководства командой программёров явно недостаточно для формирования сколько-нибудь значимого мнения.


  1. Fox_exe
    28.09.2019 16:25

    Собственно, давно использую «Bash for windows», который идет в составе Git'а.
    Ставится в пару кликов без каких-либо костылей.
    Для Сисадминов, что привыкли иметь дело с Linux — вещь незаменимая и куда более удобная, чем PowerShell (К которому ещё и привыкать надо).


    1. EvgeniyNuAfanasievich
      29.09.2019 12:16
      +1

      У bash есть взаимодействие с AD, преобразование объектов в Sid и обратно и вот это всё?


      1. Fox_exe
        29.09.2019 13:27

        Ну совсем без костылей не обойтись, да.
        Вот только Bash вполне может воспринимать команды тогоже PowerShell'а. Только с башем работать както легче…


  1. Alkop
    28.09.2019 17:01

    В Powershell очень не хватает нормальной работы с History и было бы удобно иметь что-то наподобие Ctrl+R под которой запускается «hh» от ultradvorka.


    1. ForNeVeR
      29.09.2019 18:49
      +1

      Начиная с какой-то версии они начали встраивать PSReadline, в котором Ctrl-R работает из коробки. Попробуйте :)


      1. Alkop
        29.09.2019 20:32

        в 5.1 работает точно — спасибо :)
        Но работает не так как hh от ultradvorka — выводит только одну строку в которой искомый паттерн попадается в последней команде в хистори — не то :(
        При этом Get-History всё ещё выводит только историю текущей сессии… Как так-то? :(


  1. 411
    28.09.2019 22:20
    +1

    Перешёл на WSL 2, иногда использую как раз команды powershell(например для получения информации по занятой памяти) и файлы .exe (например для копирования из vim в буфер винды) в нём, но не наоборот. У каждого конечно свои задачи, но добавить ещё сюда новый виндовый терминал и необходимость в таком решении полностью отпадёт.


  1. llia6an
    29.09.2019 03:20

    Если абстрагироваться от вопроса, какая ОС лучше, то есть несколько моментов, которые у винды неудобнее в консоли. Первое — обратные слэши. В других операционках используется обычный слэш для пути, а обратный для экранирования. Обычный легче набирать, так как он на двух кливишах и без шифтов, обратный же на одной и с шифтом. Второе — верхний регистр. Имена программ редко содержат верхний регистр, а вот ключи довольно часто, особенно powershell. В терминале сильно работаешь клавой, и эти мелочи уменьшают продуктивность. Не зря например в питоне отказались от скобок и точек с запятой, так тупо меньше и легче набирать. Все эти штуки с wsl прикольно, но это больше похоже на крик бессилия, типа возьмите нашу консоль, мы прикрутим к ней что угодно, лишь бы вы её полюбили. Я годами ждал, терпел и ждал, когда всё пойдёт хорошо, но в итоге наигрался в игры, со спокойной душой поставил минт и начал получать удовольствие от того, что нет теперь 'C:\Program Files', а есть '/usr/bin', нет 'C:\Documents and Settings\Application Data\Roaming', а есть '/home//.config'. Может я привёл не полные аналоги папок, ну я думаю вы поняли.


    1. mistergrim
      29.09.2019 05:58
      +1

      обратный же на одной и с шифтом
      С каких это пор он с шифтом? Более того, он набирается в любой раскладке одной и той же клавишей, в отличие от.
      Имена программ редко содержат верхний регистр, а вот ключи довольно часто
      Это вы точно про Windows? А то я тут ключи ls посмотрел, например.


      1. llia6an
        29.09.2019 13:13

        Вы правы, без шифта. А что с ключами ls? Да есть в верхнем регистре, но они однобуквенные, в некоторых программах много ключей, и только нижнего регистра не хватает. Открыл википедию про powershell. Таблица сравнения командлетов с аналогичными командами. Зачем писать Get-Location, если можно написать get-location. Два раза нужно нажимать шифт. Это избыточно. Нам же не любоваться потом тем, что мы там написали.


        1. mistergrim
          30.09.2019 08:09

          С ключами ls то, что -a и -A, -b и -B (и далее по списку) — это разные ключи. И такое сплошь и рядом.

          Зачем писать Get-Location, если можно написать get-location.
          Так а кто мешает? В powershell команды нечувствительны к регистру.


    1. Goodkat
      29.09.2019 11:50
      +1

      А что, в Windows 10 отменили обычные слэши в путях?
      "C:/Program Files" раньше тоже работало.
      Да и буква диска обычно не нужна, так что путь /Users/Goodkat/Documents в Windows мало отличается от /home/Goodkat/Documents в GNU/Linux.


      1. llia6an
        29.09.2019 13:15

        А что, у всех windows 10?


        1. Goodkat
          29.09.2019 13:25
          +1

          Вы о чём, вообще?


          WSL появилось в Windows 10. В статье и вашем комментарии речь о WSL.


          До Windows 10 (скорее всего и в Windows 10 тоже — было бы странным ломать обратную совместимость, но я не проверял) в путях можно использовать прямые слэши наравне с обратными.


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


        1. ForNeVeR
          29.09.2019 18:52

          Все известные мне системные API давно умели в прямые слэши в путях (кроме разных интересных случаев с UNC и т.п.). Ну то есть как минимум с XP, а то и с NT4 это работает нормально.


          Единственная существенная проблема, если вы пытаетесь перейти на использование прямых слэшей везде — это cmd.exe, он не любит такие слэши в некоторых случаях (в большинстве?). А всё остальное обычно работает.


      1. ForNeVeR
        29.09.2019 18:56

        Буква диска обычно не нужна, только если у вас всё на одном диске.


        Само по себе утверждение, на мой взгляд, опасное — вон, ребята в Meteor в течение очень долгого времени не могли починить старт приложения с любого другого диска, кроме C — из-за того, что думали сходным образом :)


        Работать с путями стоит очень осторожно. Хотя если вы не разрабатываете софт, а пользуетесь уже готовым, в рамках повседневной деятельности можно спокойно передавать пути без указания диска. Вы верно заметили, что это должно работать.


        1. Goodkat
          29.09.2019 19:32
          +1

          Речь идёт о работе в консоли, когда приходится руками набирать пути.


          1. ForNeVeR
            29.09.2019 19:50

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


            Ну, мало ли, вдруг кто-то из участников дискуссии считает, что уже давно на всех Windows-машинах диск остался только один? :)


            Это ни в коем случае не возражение, а скорее дополнение к сказанному вами.


  1. sved
    29.09.2019 03:43

    Странно пользователям Windows жаловаться на отсутствие утилит grep и sed.
    Ведь их можно просто установить .

    Ну Cygwin наше всё. Без него вообще никуда, я считаю.
    Полезные графические утилиты там тоже присутствуют. Мне например нравится baobab


    1. justhabrauser
      29.09.2019 12:10
      +2

      Статье не хватает тега «Троллейбус из буханки хлеба».
      Это не про удобство работы.


      1. llia6an
        29.09.2019 13:21

        О чём и речь.


  1. teakettle
    30.09.2019 15:10

    Реквестирую вставку apt-get update && apt-get upgrade, чтобы оно научилось все-таки нормально обновляться…