Domanda tipica degli sviluppatori Windows: «Perché qui non c'è ancora <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>?». Будь то мощное пролистывание less o strumenti familiari grep o sed, gli sviluppatori Windows vogliono avere un facile accesso a questi comandi nel loro lavoro quotidiano.
ha fatto un enorme passo avanti in questo senso. Consente di eseguire comandi Linux da Windows, inoltrandoli tramite wsl.exe (ad esempio, wsl ls). Anche se questo è un miglioramento significativo, questa opzione presenta diversi svantaggi.
- L'aggiunta diffusa
wslè noiosa e innaturale. - I percorsi Windows nei parametri non funzionano sempre, poiché le barre rovesciate vengono interpretate come caratteri di escape anziché come delimitatori di directory.
- I percorsi Windows nei parametri non vengono convertiti nel corrispondente punto di montaggio in WSL.
- Non vengono considerati i parametri predefiniti nei profili WSL con alias e variabili ambientali.
- Non è supportato il completamento dei percorsi Linux.
- Non è supportato il completamento dei comandi.
- Non è supportato il completamento degli argomenti.
Di conseguenza, i comandi Linux vengono percepiti su Windows come cittadini di seconda classe — e sono più difficili da usare rispetto ai comandi nativi. Per equilibrare i diritti, è necessario risolvere i problemi elencati.
Le funzioni di PowerShell
Con le funzioni di PowerShell possiamo aggiungere il completamento automatico dei comandi ed eliminare la necessità di prefissi wsl, traducendo i percorsi Windows in percorsi WSL. I requisiti principali per le funzioni sono:
- Per ogni comando Linux deve esserci una funzione con lo stesso nome.
- La funzione deve riconoscere i percorsi Windows passati come parametri e convertirli in percorsi WSL.
- La funzione deve chiamare
wslcon il corrispondente comando Linux su qualsiasi input della pipeline e passando eventuali argomenti della riga di comando ricevuti dalla funzione.
Poiché questo schema può essere applicato a qualsiasi comando, possiamo astrarre la definizione di queste funzioni e generarle dinamicamente da un elenco di comandi da importare.
# 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 ' ')
}
}
"@
} Elenco $command definisce i comandi da importare. Quindi generiamo dinamicamente un wrapper di funzione per ognuno di essi, utilizzando il comando Invoke-Expression (prima rimuovendo eventuali alias che potrebbero entrare in conflitto con la funzione).
La funzione esamina gli argomenti della riga di comando, identificando i percorsi Windows con i comandi Split-Path e Test-Path, e poi converte questi percorsi in percorsi WSL. Passiamo i percorsi attraverso una funzione ausiliaria Format-WslArgument, che definiremo in seguito. Essa esegue l'escaping dei caratteri speciali, come spazi e parentesi, che altrimenti verrebbero interpretati in modo errato.
Infine, passiamo wsl i dati della pipeline e eventuali argomenti da riga di comando.
Con tali wrapper è possibile invocare i comandi Linux preferiti in un modo più naturale, senza aggiungere un prefisso wsl e senza preoccuparsi di come vengono convertiti i percorsi:
man bashless -i $profile.CurrentUserAllHostsls -Al C:Windows | lessgrep -Ein error *.logtail -f *.log
Qui è mostrato un insieme di comandi di base, ma puoi creare un wrapper per qualsiasi comando Linux semplicemente aggiungendolo alla lista. Se aggiungi questo codice al tuo PowerShell, questi comandi saranno disponibili in ogni sessione PowerShell, proprio come i comandi nativi!
Parametri predefiniti
In Linux è comune definire alias e/o variabili d'ambiente nei profili (login profile), impostando parametri predefiniti per i comandi frequentemente utilizzati (ad esempio, alias ls=ls -AFh o export LESS=-i). Uno degli svantaggi dell'inoltro attraverso una shell non interattiva wsl.exe è che i profili non vengono caricati, quindi questi parametri predefiniti non sono disponibili (cioè, ls in WSL e wsl ls comportandosi in modo diverso con l'alias definito sopra).
PowerShell offre , un meccanismo standard per definire i parametri predefiniti, ma solo per i cmdlet e le funzione avanzate. Certamente, possiamo trasformare i nostri wrapper in funzioni avanzate, ma ciò introduce una complessità aggiuntiva (ad esempio, PowerShell associa i nomi parziali dei parametri (come, -a associato a -ArgumentList), che entreranno in conflitto con i comandi Linux che accettano nomi parziali come argomenti), e la sintassi per definire i valori predefiniti non sarà la più adatta (per definire gli argomenti predefiniti è necessario il nome del parametro nella chiave, non solo il nome del comando).
Tuttavia, con una piccola modifica dei nostri wrapper possiamo implementare un modello simile $PSDefaultParameterValues, e includere i parametri predefiniti per i comandi 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 ' ')
}
} Passando $WslDefaultParameterValues nella riga di comando, inviamo i parametri tramite wsl.exe. Di seguito viene mostrato come aggiungere le istruzioni al profilo PowerShell per configurare i parametri predefiniti. Ora possiamo farlo!
$WslDefaultParameterValues["grep"] = "-E"
$WslDefaultParameterValues["less"] = "-i"
$WslDefaultParameterValues["ls"] = "-AFh --group-directories-first" Poiché i parametri sono modellati dopo $PSDefaultParameterValues, puoi temporaneamente impostando la chiave "Disabled" a valore $true. Un ulteriore vantaggio di una tabella hash separata è la possibilità di disabilitare $WslDefaultParameterValues separatamente da $PSDefaultParameterValues.
Il completamento automatico degli argomenti
PowerShell consente di registrare i completatori di argomenti usando il comando Register-ArgumentCompleter. In Bash ci sono potenti . WSL consente di richiamare bash da PowerShell. Se possiamo registrare i completatori di argomenti per le nostre funzioni della shell PowerShell e richiamare bash per generare completamenti, otteniamo un completamento automatico degli argomenti con la stessa precisione di 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]
}
}Il codice è un po' denso senza comprendere alcune funzioni interne di bash, ma fondamentalmente facciamo quanto segue:
- Registriamo il completatore di argomenti per tutte le nostre funzioni wrapper, passando una lista
$commandsal parametro-CommandNameperRegister-ArgumentCompleter. - Associamo ogni comando alla funzione della shell che bash utilizza per il completamento automatico (per definire le specifiche del completamento automatico in bash si utilizza
$F, abbreviazione dicomplete -F). - Convertiamo gli argomenti PowerShell
$wordToComplete,$commandAste$cursorPositionnel formato atteso dalle funzioni di completamento automatico di bash secondo le specifiche bash. - Compiliamo la riga di comando da passare a
wsl.exe, che garantisce la corretta configurazione dell'ambiente, chiama la funzione di completamento automatico corrispondente e restituisce i risultati suddivisi in righe. - Poi chiamiamo
wslcon la riga di comando, dividiamo l'output con i delimitatori di riga e generiamo per ciascunoCompletionResults, ordinandoli e sfuggendo ai caratteri come spazi e parentesi che altrimenti sarebbero interpretati in modo errato.
Alla fine, le nostre shell dei comandi Linux utilizzeranno esattamente lo stesso completamento automatico di bash! Ad esempio:
ssh -c -J -m -O -o -Q -w -b
Ogni completamento automatico fornisce valori specifici per l'argomento precedente, leggendo i dati di configurazione, come gli host conosciuti, da WSL!
<TAB> itererà ciclicamente i parametri. <Ctrl + пробел> mostrerà tutte le opzioni disponibili.
Inoltre, poiché ora il completamento automatico bash è attivo, puoi completare automaticamente i percorsi Linux direttamente in PowerShell!
less /etc/ls /usr/share/vim ~/ .bash
Nei casi in cui il completamento automatico bash non restituisca risultati, PowerShell torna al sistema predefinito con i percorsi di Windows. In questo modo, puoi effettivamente utilizzare entrambi i percorsi a tuo piacimento.
Conclusione
Con PowerShell e WSL possiamo integrare comandi Linux in Windows come applicazioni native. Non è necessario cercare build Win32 o utilità Linux o interrompere il flusso di lavoro spostandosi nella shell di Linux. Basta , configurare e ! Un ricco completamento automatico per i parametri dei comandi e i percorsi ai file Linux e Windows è una funzionalità che oggi non è nemmeno presente nei comandi nativi di Windows.
Il codice sorgente completo, descritto sopra, e ulteriori suggerimenti per la sua integrazione nel flusso di lavoro sono disponibili .
Quali comandi Linux consideri più utili? Cosa manca ancora di familiare quando si lavora in Windows? Scrivilo nei commenti o !
Fonte: habr.com
