Typisk spørsmål fra Windows-utviklere: «Hvorfor er det fortsatt ikke <SETT INN DIN FAVORITT LINUX-KOMMANDO HER>?». Будь то мощное пролистывание less eller kjente verktøy grep eller sed, Windows-utviklere ønsker lett tilgang til disse kommandoene i sitt daglige arbeid.
har gjort store fremskritt i denne sammenhengen. Det tillater å kjøre Linux-kommandoer fra Windows, ved å proxy dem gjennom wsl.exe (for eksempel, wsl ls). Selv om dette er en betydelig forbedring, lider denne metoden av en rekke ulemper.
- Utvidelsen av
wsler slitsom og unaturlig. - Windows-stier i argumentene fungerer ikke alltid som de skal, fordi bakover-/fireslashes blir tolket som escape-tegn, ikke som katalogskillelinjer.
- Windows-stier i argumentene oversettes ikke til den tilsvarende monteringspunktet i WSL.
- Standardparametere i WSL-profiler med aliaser og miljøvariabler tas ikke hensyn til.
- Fullføring av Linux-stier støttes ikke.
- Fullføring av kommandoer støttes ikke.
- Fullføring av argumenter støttes ikke.
Som et resultat blir Linux-kommandoer oppfattet som annenrangs borgere i Windows — og de er vanskeligere å bruke enn de innebygde kommandoene. For å likestille dem, må vi løse de nevnte problemene.
PowerShell-funksjoner
Med hjelpe av PowerShell-funksjoner kan vi legge til autocompletion av kommandoer og eliminere behovet for prefikser wsl, ved å oversette Windows-stier til WSL-stier. De viktigste kravene til funksjoner er:
- Det må være én funksjon for hver Linux-kommando med samme navn.
- Funksjonen må gjenkjenne Windows-stier som sendes inn som argumenter, og konvertere dem til WSL-stier.
- Funksjonen må kalle
wslmed den tilhørende Linux-kommandoen på enhver inngang i røret og sende videre alle argumentene fra kommandolinjen til funksjonen.
Siden denne malen kan anvendes på hvilken som helst kommando, kan vi abstrahere definisjonen av disse funksjonene og dynamisk generere dem fra en liste over kommandoer for 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 ' ')
}
}
"@
} Liste $command definerer kommandoene for import. Så genererer vi dynamisk en funksjonswrapping for hver av dem, ved å bruke kommandoen Invoke-Expression (først fjerner vi eventuelle aliaser som kan konflikte med funksjonen).
Funksjonen itererer over kommandolinjeargumentene og gjenkjenner Windows-stier med kommandoene Split-Path og Test-Path, og deretter konverterer disse stiene til WSL-stier. Vi kjører stiene gjennom en hjelpefunksjon Format-WslArgument, som vi skal definere senere. Den unnslipper spesialtegn, som mellomrom og parenteser, som ellers ville blitt tolket feil.
Til slutt overfører vi wsl pipeline-inndata og eventuelle kommandolinjeargumenter.
Med slike innpakninger kan du påkalle favorittkommandoene dine i Linux på en mer naturlig måte, uten å legge til prefikser wsl og uten bekymringer om hvordan stier konverteres:
man bashless -i $profile.CurrentUserAllHostsls -Al C:Windows | lessgrep -Ein error *.logtail -f *.log
Her vises et grunnleggende sett med kommandoer, men du kan lage en innpakning for enhver Linux-kommando ved å legge den til listen. Hvis du legger denne koden i din PowerShell, vil disse kommandoene være tilgjengelige for deg i hver PowerShell-økt, akkurat som innebygde kommandoer!
Standardinnstillinger
I Linux er det vanlig å definere aliaser og/eller miljøvariabler i profiler (login profile), som setter standardinnstillinger for ofte brukte kommandoer (for eksempel, alias ls=ls -AFh eller export LESS=-i). En av ulempene med å proxy gjennom en ikke-interaktiv skall wsl.exe er at profiler ikke lastes, så disse standardinnstillingene ikke er tilgjengelige (dvs. ls i WSL og wsl ls vil oppføre seg annerledes med aliaset som er definert ovenfor).
PowerShell gir , en standardmekanisme for å definere standardverdier, men bare for cmdlets og utvidede funksjoner. Selvfølgelig kan vi gjøre våre skall til utvidede funksjoner, men dette tilfører unødvendige komplikasjoner (slik at PowerShell korrelerer delvise parameter navn (for eksempel, -a koresponderer med -ArgumentList), som vil føre til konflikter med Linux-kommandoer som aksepterer delvise navn som argumenter), og syntaksen for å definere standardverdier vil ikke være den mest hensiktsmessige (for å definere gjeldende argumenter kreves parameter navn i nøkkelen, ikke bare kommando navnet).
Men med litt endring av våre skall kan vi implementere en modell som ligner $PSDefaultParameterValues, og inkludere standardinnstillinger for Linux-kommandoene!
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 ' ')
}
} Ved å overføre $WslDefaultParameterValues i kommandolinjen sender vi parametre gjennom wsl.exe. Nedenfor vises hvordan du kan legge til instruksjoner i PowerShell-profilen for å sette standard parametre. Nå kan vi gjøre dette!
$WslDefaultParameterValues["grep"] = "-E"
$WslDefaultParameterValues["less"] = "-i"
$WslDefaultParameterValues["ls"] = "-AFh --group-directories-first" Siden parametrene er modellert etter $PSDefaultParameterValues, kan du midlertidig ved å sette nøkkelen "Disabled" til verdien $true. En ekstra fordel med en egen hash-tabell er muligheten til å deaktivere $WslDefaultParameterValues separat fra $PSDefaultParameterValues.
Autfullføring av argumenter
PowerShell lar deg registrere argumentfullførere med kommandoen Register-ArgumentCompleter. I Bash finnes det kraftige . WSL lar deg kalle bash fra PowerShell. Hvis vi kan registrere argumentfullførere for funksjons-skalene i PowerShell og kalle bash for å opprette fullføringer, får vi full autfullføring av argumenter med samme presisjon som i selve 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]
}
}Koden er litt tett uten forståelse av noen interne funksjoner i bash, men stort sett gjør vi følgende:
- Vi registrerer argumentfullføreren for alle våre funksjonsinnpakninger ved å sende en liste
$commandsi parameteren-CommandNameforRegister-ArgumentCompleter. - Kobler hver kommando til funksjonen i skallet som bash bruker for autfullføring (for å definere spesifikasjonene for autfullføring i bash brukes
$F, en forkortelse forcomplete -F). - Vi konverterer argumentene i PowerShell
$wordToComplete,$commandAstog$cursorPositiontil formatet som forventes av funksjonene for autfullføring i bash i henhold til spesifikasjonene for bash. - Vi setter sammen kommandolinjen for å sende til
wsl.exe, som sørger for riktig oppsett av miljøet, kaller den aktuelle autfullføringsfunksjonen og viser resultatene linje for linje. - Deretter kaller vi
wslmed kommandolinjen, deler utdataene med linjeskille og genererer for hverCompletionResults, sorterer dem og rømmer tegn som mellomrom og parenteser, som ellers ville blitt feiltolket.
Til slutt vil kommandoskallet våre Linux bruke nøyaktig samme autfullføring som i bash! For eksempel:
ssh -c -J -m -O -o -Q -w -b
Hver autocompletion gir verdier spesifik for det forrige argumentet, ved å lese konfigurasjonsdata som kjente verter fra WSL!
<TAB> vil sykle gjennom alternativene. <Ctrl + пробел> vil vise alle tilgjengelige alternativer.
I tillegg, siden vi nå har autocompletion i bash, kan du autocompletere Linux-stier direkte i PowerShell!
less /etc/ls /usr/share/vim ~/ .bash
I tilfeller hvor bash-autocompletion ikke gir noen resultater, faller PowerShell tilbake på standard systemveier i Windows. Dermed kan du i praksis bruke begge typer stier etter ønsket.
Konklusjon
Med PowerShell og WSL kan vi integrere Linux-kommandoer i Windows som om de var native applikasjoner. Det er ingen behov for å lete etter Win32 builds eller Linux-verktøy, eller å avbryte arbeidsflyten din for å gå til Linux-shellet. Bare , konfigurer og ! Rik autocompletion for kommandoparametere og filstier for Linux og Windows – dette er en funksjonalitet som ikke engang finnes i native Windows-kommandoer i dag.
Den fullstendige kildekoden beskrevet ovenfor, samt ytterligere tips for å inkludere den i arbeidsflyten, er tilgjengelig .
Hvilke Linux-kommandoer anser du som mest nyttige? Hva savner du ellers av kjente ting når du jobber i Windows? Skriv i kommentarfeltet eller !
Kilde: habr.com
