Una pregunta típica de los desarrolladores de Windows: «¿Por qué aún no hay aquí <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>?». Будь то мощное пролистывание less o herramientas familiares grep o sed, los desarrolladores de Windows quieren tener un acceso fácil a estos comandos en su trabajo diario.
ha avanzado enormemente en este aspecto. Permite invocar comandos de Linux desde Windows, proxyándolos a través de wsl.exe (por ejemplo, wsl ls). Aunque es una mejora significativa, este enfoque tiene varios inconvenientes.
- La incorporación generalizada de
wsles tediosa y poco natural. - Las rutas de Windows en los argumentos no siempre funcionan, porque las barras invertidas se interpretan como caracteres de escape en lugar de separadores de directorio.
- Las rutas de Windows en los argumentos no se traducen en el punto de montaje correspondiente en WSL.
- No se consideran los parámetros predeterminados en los perfiles de WSL con alias y variables de entorno.
- No se admite la autocompletación de rutas de Linux.
- No se admite la autocompletación de comandos.
- No se admite la autocompletación de argumentos.
Como resultado, los comandos de Linux se consideran en Windows como ciudadanos de segunda clase — y son más difíciles de utilizar que los comandos nativos. Para nivelarlos, es necesario resolver los problemas mencionados.
Las funciones de PowerShell
Con las funciones de PowerShell, podemos añadir la autocompletación de comandos y eliminar la necesidad de prefijos wsl, traduciendo las rutas de Windows a rutas de WSL. Los requisitos principales para las funciones son:
- Para cada comando de Linux debe haber una función con el mismo nombre.
- La función debe reconocer las rutas de Windows pasadas como argumentos y convertirlas en rutas de WSL.
- La función debe invocar
wslcon el comando correspondiente de Linux en cualquier entrada de tubería y pasar cualquier argumento de línea de comandos que se pase a la función.
Dado que este patrón puede aplicarse a cualquier comando, podemos abstraer la definición de estas funciones y generarlas dinámicamente desde una lista de comandos para importar.
# 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 los comandos para importar. Luego, generamos dinámicamente un envoltorio de función para cada uno de ellos utilizando el comando Invoke-Expression (primero eliminando cualquier alias que pueda entrar en conflicto con la función).
La función itera sobre los argumentos de línea de comandos, identificando las rutas de Windows usando los comandos Split-Path y Test-Path, y luego convierte estas rutas en rutas WSL. Pasamos las rutas a través de una función auxiliar Format-WslArgument, que definiremos más adelante. Esta escapa caracteres especiales, como espacios y paréntesis, que de otro modo serían interpretados incorrectamente.
Finalmente, pasamos wsl la entrada del canal y cualquier argumento de línea de comandos.
Con estas envolturas, puedes llamar a tus comandos favoritos de Linux de una manera más natural, sin agregar un prefijo wsl y sin preocuparte de cómo se convierten las rutas:
man bashless -i $profile.CurrentUserAllHostsls -Al C:Windows | lessgrep -Ein error *.logtail -f *.log
Aquí se muestra un conjunto básico de comandos, pero puedes crear un envoltorio para cualquier comando de Linux simplemente agregándolo a la lista. Si agregas este código a tu PowerShell, estos comandos estarán disponibles en cada sesión de PowerShell, al igual que los comandos nativos!
Parámetros predeterminados
En Linux, es común definir alias y/o variables de entorno en perfiles (perfil de inicio de sesión), configurando parámetros predeterminados para comandos de uso frecuente (por ejemplo, alias ls=ls -AFh o export LESS=-i). Una desventaja del uso de un shell no interactivo wsl.exe es que los perfiles no se cargan, por lo que estos parámetros predeterminados no están disponibles (es decir, Muestra el contenido del directorio. Si no se proporciona una ruta, se muestra el contenido del directorio actual. en WSL y wsl ls se comportarán de manera diferente con un alias definido anteriormente).
PowerShell proporciona , un mecanismo estándar para definir parámetros predeterminados, pero solo para cmdlets y funciones avanzadas. Por supuesto, podemos convertir nuestros shells en funciones avanzadas, pero esto añade complicaciones adicionales (de hecho, PowerShell relaciona nombres parciales de parámetros (por ejemplo, -a se relaciona con -ArgumentList), que entrarán en conflicto con los comandos de Linux que aceptan nombres parciales como argumentos), y la sintaxis para definir valores predeterminados no será la más adecuada (para definir argumentos predeterminados se requiere el nombre del parámetro en la clave, y no solo el nombre del comando).
Sin embargo, con un pequeño cambio en nuestros shells podemos implementar un modelo similar $PSDefaultParameterValues, ¡y habilitar parámetros predeterminados para los comandos de 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 ' ')
}
} Al pasar $WslDefaultParameterValues en la línea de comandos, enviamos parámetros a través de wsl.exe. A continuación se muestra cómo agregar instrucciones al perfil de PowerShell para configurar los valores predeterminados. ¡Ahora podemos hacerlo!
$WslDefaultParameterValues["grep"] = "-E"
$WslDefaultParameterValues["less"] = "-i"
$WslDefaultParameterValues["ls"] = "-AFh --group-directories-first" Dado que los parámetros se modelan según $PSDefaultParameterValues, puedes temporalmente estableciendo la clave "Disabled" en $true. Otra ventaja de tener una tabla hash separada es la capacidad de desactivar $WslDefaultParameterValues independientemente de $PSDefaultParameterValues.
El autocompletado de argumentos
PowerShell permite registrar completas de argumentos utilizando el comando Register-ArgumentCompleter. Bash tiene poderosas . WSL permite llamar a bash desde PowerShell. Si podemos registrar completas de argumentos para nuestras funciones de PowerShell y llamar a bash para generar completaciones, obtendremos un autocompletado completo de argumentos con la misma precisión que en bash mismo!
# 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]
}
}El código es un poco denso sin entender algunas funciones internas de bash, pero en su mayor parte hacemos lo siguiente:
- Registramos un completador de argumentos para todas nuestras funciones envolventes, pasando una lista
$commandsal parámetro-CommandNameparaRegister-ArgumentCompleter. - Vinculamos cada comando con la función de shell que bash utiliza para el autocompletado (para determinar las especificaciones de completado en bash se utiliza
$F, una abreviatura decomplete -F). - Transformamos los argumentos de PowerShell
$wordToComplete,$commandAsty$cursorPositional formato esperado por las funciones de autocompletado de bash de acuerdo con las especificaciones bash. - Construimos la línea de comandos para pasar a
wsl.exe, que garantiza la configuración correcta del entorno, llama a la función de autocompletado correspondiente y devuelve los resultados separados por líneas. - Luego llamamos a
wslcon la línea de comandos, separamos la salida por delimitadores de línea y generamos para cadaCompletionResults, ordenándolos y escapando caracteres como espacios y paréntesis que de otro modo serían malinterpretados.
¡Al final, nuestros shells de comandos de Linux utilizarán exactamente el mismo autocompletado que en bash! Por ejemplo:
ssh -c -J -m -O -o -Q -w -b
Cada autocompletar proporciona valores específicos para el argumento anterior, leyendo datos de configuración como hosts conocidos de WSL.
<TAB> se iterará cíclicamente sobre los parámetros. <Ctrl + пробел> mostrará todas las opciones disponibles.
Además, dado que ahora tenemos autocompletar de bash funcionando, ¡puedes autocompletar rutas de Linux directamente en PowerShell!
less /etc/ls /usr/share/vim ~/ .bash
En los casos en que el autocompletar de bash no devuelva resultados, PowerShell vuelve al sistema predeterminado con rutas de Windows. Por lo tanto, puedes utilizar ambos tipos de rutas a tu conveniencia.
Conclusión
Con PowerShell y WSL, podemos integrar comandos de Linux en Windows como aplicaciones nativas. No hay necesidad de buscar compilaciones de Win32 o utilidades de Linux o interrumpir tu flujo de trabajo cambiando al shell de Linux. Simplemente , configura y ! Un autocompletar rico para los parámetros de comandos y rutas de archivos de Linux y Windows es una funcionalidad que hoy en día ni siquiera tienen los comandos nativos de Windows.
El código fuente completo descrito anteriormente, así como recomendaciones adicionales para su inclusión en el flujo de trabajo, están disponibles .
¿Cuáles son los comandos de Linux que consideras más útiles? ¿Qué otras cosas familiares faltan al trabajar en Windows? Déjanos tus comentarios o !
Fuente: habr.com
