Mengintegrasikan perintah Linux ke Windows menggunakan PowerShell dan WSL

Pertanyaan khas dari pengembang Windows: “Mengapa masih belum ada <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>?. Baik itu gesekan yang kuat less atau alat yang familiar grep или sed, Pengembang Windows menginginkan akses mudah ke perintah ini dalam pekerjaan sehari-hari mereka.

Subsistem Windows untuk Linux (WSL) telah membuat langkah maju yang besar dalam hal ini. Ini memungkinkan Anda untuk memanggil perintah Linux dari Windows dengan mem-proxy-nya wsl.exe (mis. wsl ls). Meskipun ini merupakan kemajuan yang signifikan, opsi ini mempunyai sejumlah kelemahan.

  • Tambahan di mana-mana wsl membosankan dan tidak wajar.
  • Jalur Windows dalam argumen tidak selalu berfungsi karena garis miring terbalik ditafsirkan sebagai karakter escape, bukan pemisah direktori.
  • Jalur Windows dalam argumen tidak diterjemahkan ke titik pemasangan yang sesuai di WSL.
  • Pengaturan default tidak diterapkan di profil WSL dengan alias dan variabel lingkungan.
  • Penyelesaian jalur Linux tidak didukung.
  • Penyelesaian perintah tidak didukung.
  • Penyelesaian argumen tidak didukung.

Akibatnya, perintah Linux diperlakukan seperti warga kelas dua di Windows—dan lebih sulit digunakan dibandingkan perintah asli. Untuk menyamakan hak-hak mereka, masalah-masalah di atas perlu diselesaikan.

Pembungkus fungsi PowerShell

Dengan pembungkus fungsi PowerShell, kita dapat menambahkan penyelesaian perintah dan menghilangkan kebutuhan akan prefiks wsl, menerjemahkan jalur Windows ke jalur WSL. Persyaratan dasar untuk cangkang:

  • Untuk setiap perintah Linux harus ada satu pembungkus fungsi dengan nama yang sama.
  • Shell harus mengenali jalur Windows yang diteruskan sebagai argumen dan mengubahnya menjadi jalur WSL.
  • Shell harus menelepon wsl dengan perintah Linux yang sesuai ke masukan saluran apa pun dan meneruskan argumen baris perintah apa pun yang diteruskan ke fungsi.

Karena pola ini dapat diterapkan pada perintah apa pun, kita dapat mengabstraksikan definisi pembungkus ini dan secara dinamis menghasilkannya dari daftar perintah yang akan diimpor.

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

Daftar $command mendefinisikan perintah impor. Kami kemudian secara dinamis membuat pembungkus fungsi untuk masing-masing fungsi tersebut menggunakan perintah Invoke-Expression (dengan terlebih dahulu menghapus alias yang bertentangan dengan fungsi tersebut).

Fungsi ini mengulangi argumen baris perintah, menentukan jalur Windows menggunakan perintah Split-Path и Test-Pathdan kemudian mengubah jalur ini menjadi jalur WSL. Kami menjalankan jalur melalui fungsi pembantu Format-WslArgument, yang akan kita definisikan nanti. Ini lolos dari karakter khusus seperti spasi dan tanda kurung yang dapat disalahartikan.

Terakhir, kami sampaikan wsl masukan pipa dan argumen baris perintah apa pun.

Dengan wrapper ini Anda dapat memanggil perintah Linux favorit Anda dengan cara yang lebih alami tanpa menambahkan awalan wsl dan tanpa khawatir tentang bagaimana jalur dikonversi:

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

Kumpulan perintah dasar ditampilkan di sini, tetapi Anda dapat membuat shell untuk perintah Linux apa pun hanya dengan menambahkannya ke daftar. Jika Anda menambahkan kode ini ke profil PowerShell, perintah ini akan tersedia untuk Anda di setiap sesi PowerShell, sama seperti perintah asli!

Pengaturan Bawaan

Di Linux, merupakan hal yang umum untuk mendefinisikan alias dan/atau variabel lingkungan dalam profil login, mengatur parameter default untuk perintah yang sering digunakan (misalnya, alias ls=ls -AFh или export LESS=-i). Salah satu kelemahan proksi melalui shell non-interaktif wsl.exe - bahwa profil tidak dimuat, sehingga opsi ini tidak tersedia secara default (mis. ls di WSL dan wsl ls akan berperilaku berbeda dengan alias yang ditentukan di atas).

PowerShell menyediakan $PSDefaultParameterValues, mekanisme standar untuk menentukan parameter default, tetapi hanya untuk cmdlet dan fungsi lanjutan. Tentu saja, kita dapat membuat fungsi lanjutan dari shell kita, tetapi hal ini menimbulkan komplikasi yang tidak perlu (misalnya, PowerShell mengkorelasikan sebagian nama parameter (misalnya, -a berkorelasi dengan -ArgumentList), yang akan bertentangan dengan perintah Linux yang menggunakan sebagian nama sebagai argumen), dan sintaksis untuk mendefinisikan default tidak akan menjadi yang paling tepat (mendefinisikan argumen default memerlukan nama parameter dalam kunci, bukan hanya nama perintah).

Namun, dengan sedikit modifikasi pada cangkang kami, kami dapat mengimplementasikan model serupa $PSDefaultParameterValues, dan aktifkan opsi default untuk perintah 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 ' ')
    }
}

Lewat $WslDefaultParameterValues ke baris perintah, kami mengirim parameter melalui wsl.exe. Berikut ini menunjukkan cara menambahkan instruksi ke profil PowerShell Anda untuk mengonfigurasi pengaturan default. Sekarang kita bisa melakukannya!

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

Karena parameter dimodelkan setelahnya $PSDefaultParameterValueskamu bisa mudah untuk menonaktifkannya sementara dengan memasang kunci "Disabled" ke dalam makna $true. Manfaat tambahan dari tabel hash terpisah adalah kemampuan untuk menonaktifkan $WslDefaultParameterValues secara terpisah dari $PSDefaultParameterValues.

Penyelesaian argumen

PowerShell memungkinkan Anda mendaftarkan cuplikan argumen menggunakan perintah Register-ArgumentCompleter. Bash sangat kuat alat pelengkapan otomatis yang dapat diprogram. WSL memungkinkan Anda memanggil bash dari PowerShell. Jika kita dapat mendaftarkan penyelesaian argumen untuk pembungkus fungsi PowerShell dan memanggil bash untuk menghasilkan penyelesaian, kita mendapatkan penyelesaian argumen penuh dengan presisi yang sama seperti bash itu sendiri!

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

Kode ini agak padat tanpa memahami beberapa fungsi internal bash, tapi pada dasarnya apa yang kita lakukan adalah ini:

  • Mendaftarkan pelengkap argumen untuk semua pembungkus fungsi kami dengan meneruskan daftar $commands dalam parameter -CommandName untuk Register-ArgumentCompleter.
  • Kami memetakan setiap perintah ke fungsi shell yang digunakan bash untuk pelengkapan otomatis (untuk menentukan spesifikasi pelengkapan otomatis, bash menggunakan $F, singkatan dari complete -F <FUNCTION>).
  • Mengonversi Argumen PowerShell $wordToComplete, $commandAst и $cursorPosition ke dalam format yang diharapkan oleh fungsi pelengkapan otomatis bash sesuai dengan spesifikasi penyelesaian otomatis yang dapat diprogram pesta.
  • Kami membuat baris perintah untuk mentransfer wsl.exe, yang memastikan bahwa lingkungan diatur dengan benar, memanggil fungsi pelengkapan otomatis yang sesuai, dan menampilkan hasilnya baris demi baris.
  • Lalu kami menelepon wsl dengan baris perintah, kami memisahkan output dengan pemisah baris dan menghasilkan untuk masing-masingnya CompletionResults, mengurutkannya dan menghilangkan karakter seperti spasi dan tanda kurung yang dapat disalahartikan.

Hasilnya, shell perintah Linux kami akan menggunakan pelengkapan otomatis yang sama persis dengan bash! Misalnya:

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

Setiap pelengkapan otomatis memberikan nilai khusus untuk argumen sebelumnya, membaca data konfigurasi seperti host yang diketahui dari WSL!

<TAB> akan menggilir parameter. <Ctrl + пробел> akan menampilkan semua opsi yang tersedia.

Selain itu, karena kami sekarang memiliki pelengkapan otomatis bash, Anda dapat melengkapi jalur Linux secara otomatis langsung di PowerShell!

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

Jika pelengkapan otomatis bash tidak membuahkan hasil apa pun, PowerShell akan kembali ke jalur default sistem Windows. Jadi, dalam praktiknya, Anda dapat menggunakan kedua jalur secara bersamaan sesuai kebijaksanaan Anda.

Kesimpulan

Menggunakan PowerShell dan WSL, kita dapat mengintegrasikan perintah Linux ke Windows sebagai aplikasi asli. Tidak perlu mencari build Win32 atau utilitas Linux atau mengganggu alur kerja Anda dengan membuka shell Linux. Hanya instal WSL, konfigurasikan Profil PowerShell и daftar perintah yang ingin Anda impor! Pelengkapan otomatis yang kaya untuk parameter perintah Linux dan Windows serta jalur file adalah fungsionalitas yang bahkan tidak tersedia dalam perintah asli Windows saat ini.

Kode sumber lengkap yang dijelaskan di atas, serta pedoman tambahan untuk memasukkannya ke dalam alur kerja Anda, telah tersedia di sini.

Perintah Linux manakah yang menurut Anda paling berguna? Hal umum apa lagi yang hilang saat bekerja di Windows? Tulis di komentar atau di GitHub!

Sumber: www.habr.com

Tambah komentar