Întrebarea tipică a dezvoltatorilor de Windows: „De ce nu există încă <INSERaȚI AICI COMANDA PREFERATĂ LINUX>?». Будь то мощное пролистывание less sau unelte cunoscute grep sau sed, dezvoltatorii de Windows doresc acces ușor la aceste comenzi în activitatea zilnică.
a făcut un pas uriaș în acest sens. Acesta permite apelarea comenzilor Linux din Windows, prin intermediul wsl.exe (de exemplu, wsl ls). Deși este o îmbunătățire semnificativă, acest mod suferă de o serie de dezavantaje.
- Adăugarea pe scară largă
wsleste obositoare și nenaturală. - Cărțile Windows în argumente nu funcționează întotdeauna, deoarece backslash-urile sunt interpretate ca simboluri de evadare, nu ca delimitatori de directoare.
- Cărțile Windows în argumente nu se traduc în punctul de montare corespunzător în WSL.
- Parametrii default din profilele WSL cu aliasuri și variabile de mediu nu sunt considerați.
- Completarea căilor Linux nu este suportată.
- Completarea comenzilor nu este suportată.
- Completarea argumentelor nu este suportată.
Ca rezultat, comenzile Linux sunt percepute în Windows ca cetățeni de rang inferior — și sunt mai greu de utilizat decât comenzile native. Pentru a le echilibra drepturile, trebuie să rezolvăm problemele enumerate.
Shell-urile funcțiilor PowerShell
Prin intermediul shell-urilor funcțiilor PowerShell putem adăuga completarea automată a comenzilor și elimina necesitatea prefixelor wsl, translatând căile Windows în căile WSL. Cerințele de bază pentru shell-uri sunt:
- Pentru fiecare comandă Linux ar trebui să existe un shell de funcție cu același nume.
- Shell-ul trebuie să recunoască căile Windows transmise ca argumente și să le convertească în căi WSL.
- Shell-ul trebuie să invoce
wslcomanda Linux corespunzătoare pentru orice intrare din canal și să transmită orice argumente de linie de comandă transmise funcției.
Deoarece acest șablon poate fi aplicat oricărei comenzi, putem abstractiza definiția acestor shell-uri și le putem genera dinamic dintr-o listă de comenzi pentru import.
# 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 ' ')
}
}
"@
} Lista $command definește comenzile pentru import. Apoi, generăm dinamic o învăluire a funcției pentru fiecare dintre ele, folosind comanda Invoke-Expression (eliminând întâi orice aliasuri care vor intră în conflict cu funcția).
Funcția parcurge argumentele din linia de comandă, determină căile Windows folosind comenzile Split-Path și Test-Path, și apoi transformă aceste căi în căi WSL. Rulăm căile printr-o funcție auxiliară Format-WslArgument, pe care o vom defini mai târziu. Aceasta escapează caractere speciale, cum ar fi spațiile și parantezele, care altfel ar fi interpretate greșit.
În cele din urmă, transmitem wsl datele de intrare ale pipeline-ului și orice argumente de linie de comandă.
Folosind astfel de wrapper-e, poți apela comenzile tale preferate Linux într-un mod mai natural, fără a adăuga un prefix wsl și fără a te îngrijora cum sunt transformate căile:
man bashless -i $profile.CurrentUserAllHostsls -Al C:Windows | lessgrep -Ein error *.logtail -f *.log
Aici este setul de bază de comenzi, dar poți crea un wrapper pentru orice comandă Linux, adăugând-o pur și simplu în listă. Dacă adaugi acest cod în PowerShell, aceste comenzi îți vor fi disponibile în fiecare sesiune PowerShell, la fel ca comenzile native!
Parametrii impliciți
În Linux, este obișnuit să definești alias-uri și/sau variabile de mediu în profile (login profile), stabilind parametrii impliciți pentru comenzile frecvent utilizate (de exemplu, alias ls=ls -AFh sau export LESS=-i). O unul dintre dezavantajele proxy-ului printr-o shell neinteractivă wsl.exe este că profilele nu sunt încărcate, prin urmare acești parametrii implicați nu sunt disponibili (adică, ls în WSL și wsl ls se comportă diferit cu alias-ul definit mai sus).
PowerShell oferă , un mecanism standard pentru definirea parametrilor implicați, dar doar pentru cmdlets și funcții avansate. Desigur, putem transforma shell-urile noastre în funcții avansate, dar asta introduce complicații suplimentare (deci, PowerShell corelează numele parțiale ale parametrilor (de exemplu, -a corelează cu -ArgumentList), care vor intra în conflict cu comenzile Linux ce acceptă nume parțiale ca argumente), iar sintaxa pentru definirea valorilor implicite nu va fi cea mai potrivită (pentru a defini argumentele implicite este necesar numele parametrului în cheie, nu doar numele comenzii).
Cu toate acestea, cu o mică modificare a shell-urilor noastre putem implementa un model similar $PSDefaultParameterValues, și include parametrii implicați pentru comenzile 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 ' ')
}
} Transmițând $WslDefaultParameterValues în linia de comandă, trimitem parametrii prin wsl.exe. Iată cum putem adăuga instrucțiuni în profilul PowerShell pentru a configura parametrii impliciți. Acum putem face asta!
$WslDefaultParameterValues["grep"] = "-E"
$WslDefaultParameterValues["less"] = "-i"
$WslDefaultParameterValues["ls"] = "-AFh --group-directories-first" Deoarece parametrii sunt modelați după $PSDefaultParameterValues, puteți temporar, setând cheia "Disabled" to the value $true. Un alt avantaj al unui tabel hash separat este capacitatea de a dezactiva $WslDefaultParameterValues separat de $PSDefaultParameterValues.
Autocompletarea argumentelor
PowerShell permite înregistrarea completorilor de argumente cu comanda Register-ArgumentCompleter. În Bash există instrumente puternice . WSL permite apelarea bash din PowerShell. Dacă putem înregistra completorii de argumente pentru funcțiile noastre din PowerShell și apela bash pentru a crea completări, vom obține o autocompletare completă a argumentelor cu aceeași precizie ca în 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]
}
}Codul este puțin dens fără a înțelege anumite funcții interne bash, dar în principal facem următoarele:
- Înscriem completorul de argumente pentru toate funcțiile noastre, având o listă
$commandsîn parametrul-CommandNamepentruRegister-ArgumentCompleter. - Mapăm fiecare comandă la funcția shell utilizată de bash pentru autocompletare (pentru a determina specificațiile de autocompletare în bash, se folosește
$F, prescurtare pentrucomplete -F). - Transformăm argumentele PowerShell
$wordToComplete,$commandAstși$cursorPositionîntr-un format așteptat de funcțiile de autocompletare bash conform specificațiilor bash. - Construim linia de comandă pentru a fi transmisă în
wsl.exe, care asigură configurarea corectă a mediului, apelează funcția corespunzătoare de autocompletare și returnează rezultatele cu împărțirea pe linii. - Apoi apelăm
wslcu linia de comandă, împărțim ieșirea folosind separatori de linie și generăm pentru fiecareCompletionResults, sortându-le și escapând caractere precum spațiile și parantezele, care în alte condiții ar fi fost interpretate greșit.
În final, shell-urile noastre Linux vor folosi exact aceeași autocompletare ca în bash! De exemplu:
ssh -c -J -m -O -o -Q -w -b
Fiecare autocompletare furnizează valori specifice pentru argumentul anterior, citind date de configurare, cum ar fi gazdele cunoscute, din WSL!
<TAB> va itera ciclic prin parametrii. <Ctrl + пробел> va arăta toate opțiunile disponibile.
În plus, acum că autocompletarea bash funcționează, puteți completa automat căile Linux direct în PowerShell!
less /etc/ls /usr/share/vim ~/ .bash
În cazurile în care autocompletarea bash nu oferă rezultate, PowerShell revine la sistemul implicit cu căile Windows. Astfel, practicați simultan ambele căi la discreția dvs.
Concluzie
Cu PowerShell și WSL, putem integra comenzile Linux în Windows ca aplicații native. Nu mai este necesar să căutați build-uri Win32 sau utilitare Linux sau să întrerupeți fluxul de lucru trecând în shell-ul Linux. Pur și simplu , configurați și ! Autocompletarea bogată pentru parametrii comenzilor și căile fișierelor Linux și Windows este o funcționalitate care nu este disponibilă chiar și în comenzile native Windows de astăzi.
Codul sursă complet, descris mai sus, precum și recomandări suplimentare pentru includerea acestuia în fluxul de lucru sunt disponibile .
Care comenzi Linux considerați că sunt cele mai utile? Ce lucruri obișnuite mai lipsește în lucrul cu Windows? Scrieți în comentarii sau !
Sursa: habr.com
